# XMTP Documentation for Building Chat Applications Generated at 05:48 PM UTC / November 13, 2025 ## About this file This documentation is tailored for developers building chat apps with XMTP. It includes guides on core messaging, content types, push notifications, user consent, and more, along with protocol fundamentals, network operations, and funding information. ## Instructions for AI Tools This documentation includes code samples for multiple SDKs. Please use the code samples that correspond to the SDK you are working with: - For **xmtp-react-native**, use the code samples marked with `[React Native]`. - For **xmtp-android**, use the code samples marked with `[Kotlin]`. - For **xmtp-ios**, use the code samples marked with `[Swift]`. - For **xmtp-js** in a browser environment, use the code samples marked with `[Browser]`. - For **xmtp-js** in a Node.js environment, use the code samples marked with `[Node]`. Ensure that you select the correct code block to avoid compatibility issues. ## Included Sections - chat-apps - protocol - network - fund-agents-apps --- # Section: chat-apps ## chat-apps/debug-your-app.mdx # Debug your chat app This document covers tools and features available for debugging building with XMTP, including stress testing, group chat diagnostics, logging, and network monitoring capabilities. ## XMTP Debug You can use the XMTP Debug tool to stress and burn-in test your chat app on the `local` and `dev` XMTP environments. To learn more, see [XMTP Debug](https://github.com/xmtp/libxmtp/blob/main/xmtp_debug/README.md). ## Forked group debugging tool :::tip[Preventing forks is XMTP's responsibility] This tool helps you identify forked groups in your app, but preventing forks in the first place is XMTP's responsibility. This diagnostic tool is just an aid and not a shift in responsibility to your app. ::: A conversation has `getDebugInformation`. You can use this to see: - The MLS epoch of a group chat conversation for a member - The local commit log for expert analysis - Whether a group chat is forked (no false positives) via: - `isCommitLogForked` for Browser and Node SDKs - `commitLogForkStatus` for React Native, Android and iOS SDKs The `isCommitLogForked`/`commitLogForkStatus` field provides definitive fork detection without false positives. To minimize the negative effects of spam, fork detection is active only for groups that a user has actively consented to, which is automatically true for groups/DMs they created or groups/DMs they've sent a message in. To learn more about consent, see [Support user consent](/chat-apps/user-consent/support-user-consent). :::code-group ```typescript [Browser, Node] // Get detailed debug information for a conversation const debugInfo = await conversation?.debugInfo(); console.log('Epoch:', debugInfo.epoch); console.log('Cursor:', debugInfo.cursor); console.log('Fork Status:', debugInfo.isCommitLogForked); // true, false, or undefined console.log('Local Commit Log:', debugInfo.localCommitLog); console.log('Remote Commit Log:', debugInfo.remoteCommitLog); console.log('Fork Details:', debugInfo.forkDetails); ``` ```typescript [React Native, Kotlin, Swift] // Get detailed debug information for a conversation const debugInfo = await conversation?.getDebugInformation(); console.log('Epoch:', debugInfo.epoch); console.log('Fork Status:', debugInfo.commitLogForkStatus); // 'forked', 'notForked', or 'unknown' console.log('Local Commit Log:', debugInfo.localCommitLog); console.log('Remote Commit Log:', debugInfo.remoteCommitLog); console.log('Fork Details:', debugInfo.forkDetails); ``` ::: You can also check fork status directly from conversation lists: :::code-group ```typescript [Browser, Node] // Check fork status when listing conversations const conversations = await client.conversations.list(); conversations.forEach((conversation) => { if (conversation.isCommitLogForked) { console.log(`Conversation ${conversation.id} is forked`); } }); ``` ```typescript [React Native, Kotlin, Swift] // Check fork status when listing conversations const conversations = await client.conversations.list(); conversations.forEach((conversation) => { if (conversation.commitLogForkStatus === 'forked') { console.log(`Conversation ${conversation.id} is forked`); } }); ``` ::: To learn about group chat forks, see [MLS Group State Forks: What, Why, How](https://cryspen.com/post/mls-fork-resolution/). If you believe you are experiencing a forked group, please [open an issue](https://github.com/xmtp/libxmtp/issues) in the LibXMTP repo to get support. Please include logs, the epoch, and other fork details. Forked groups are not recoverable. Your options are to: - Remove all members from the forked group and then re-add them to the group. - Start a new group. ## File logging These file logging functions enable the XMTP rust process to write directly to persistent log files that roll during every hour of active usage and provide a 6-hour window of logs. This can be especially helpful when debugging group chat errors and other complex issues. The file logs complement Android and iOS system logs, which rely on memory buffer constraints, but often don't go back far enough to help catch when an issue occurs. To use file logging, call the following static functions: - `Client.activatePersistentLibXMTPLogWriter()`: Use to activate the file logging feature. - We recommend using `LogLevel.Debug`, `LogRotation.Hourly`, and a `logMaxFiles` value of 5-10. Log files can grow to ~100mb each with active clients on debug level in an hour of usage. - `Client.getXMTPLogFilePaths()`: Use to get the full paths of log files written so far. This is useful for passing to iOS or Android Share functions. - Here are additional helper static functions in the XMTP Client object: - `deactivatePersistentLibXMTPLogWriter()` - `isLogWriterActive()` - `readXMTPLogFile()` - `clearXMTPLogs()` For an example UI implementation, see PR to [add new persistent log debug menu options](https://github.com/xmtplabs/convos-app/pull/6) to the Convos app, built with XMTP. ## Network statistics You can use these statistics to see which and how many API, identity, and streaming calls are going across the network, which can help you better manage network usage and debug potential rate limiting issues. These statistics are maintained per client instance, so each app installation has its own separate counter. Each one is a rolling counter for the entire session since the gRPC client was created. To get a snapshot of statistics at a moment in time, you can check the counter, run the action, get the counter again, and then diff the counter with the original counter. ### Get aggregated statistics To return aggregated statistics, run: - For Browser, Node, iOS, and Android SDKs: `client.debugInformation.apiAggregateStatistics()` - For React Native SDK: `client.debugInformation.aggregateStatistics` ```text Aggregate Stats: ============ Api Stats ============ UploadKeyPackage 1 FetchKeyPackage 2 SendGroupMessages 5 SendWelcomeMessages 1 QueryGroupMessages 7 QueryWelcomeMessages 0 ============ Identity ============ PublishIdentityUpdate 1 GetIdentityUpdatesV2 4 GetInboxIds 2 VerifySCWSignatures 0 ============ Stream ============ SubscribeMessages 0 SubscribeWelcomes 0 ``` ### Get an individual statistic To return an individual statistic as a number, run: - For Browser, Node, iOS, and Android SDKs: - `client.debugInformation.apiStatistics.uploadKeyPackage` to track `uploadKeyPackage` only, for example - `client.debugInformation.apiIdentityStatistics.publishIdentityUpdate` to track `publishIdentityUpdate` only, for example - For React Native SDK: - `client.debugInformation.Statistics.uploadKeyPackage` to track `uploadKeyPackage` only, for example - `client.debugInformation.IdentityStatistics.publishIdentityUpdate` to track `publishIdentityUpdate` only, for example For available individual statistics, see [Statistic descriptions](#statistic-descriptions). ### Clear statistics To clear all API, identity, and stream statistics and set them to zero, run `client.debugInformation.clearAllStatistics()`. This is useful when you want to get a clean baseline before running specific actions. It's also particularly helpful for managing memory usage on mobile devices where gRPC client caching can accumulate large statistics. ### Upload an archive of network statistics With the Browser and Node SDKs, you can upload an archive of collected network statistics. ```tsx [TypeScript] // Upload to default server (no serverUrl needed) const result1 = await client.debugInformation.uploadDebugArchive(); // Upload to custom server (serverUrl provided) const result2 = await client.debugInformation.uploadDebugArchive( 'https://my-debug-server.com/api/upload' ); ``` ### Statistic descriptions #### API statistics | Statistic | Description | | -------------------- | ------------------------------------------------------------------------------------------------------- | | UploadKeyPackage | Number of times key packages have been uploaded. | | FetchKeyPackage | Number of times key packages have been fetched. | | SendGroupMessages | Number of times messages have been sent to group chat and DM conversations. | | SendWelcomeMessages | Number of times welcome messages have been sent. | | QueryGroupMessages | Number of times queries have been made to fetch messages being sent to group chat and DM conversations. | | QueryWelcomeMessages | Number of times queries have been made to fetch welcome messages. | #### Identity statistics | Statistic | Description | | --------------------- | ---------------------------------------------------------------------------------- | | PublishIdentityUpdate | Number of times identity updates have been published. | | GetIdentityUpdatesV2 | Number of times identity updates have been fetched. | | GetInboxIds | Number of times inbox ID queries have been made. | | VerifySCWSignatures | Number of times smart contract wallet signature verifications have been performed. | #### Stream statistics | Statistic | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------- | | SubscribeMessages | Number of times message subscription requests have been made. This is streaming messages in a conversation. | | SubscribeWelcomes | Number of times welcome message subscription requests have been made. This is streaming conversations. | ## chat-apps/use-signatures.mdx # Use signatures with XMTP With XMTP, you can use various types of signatures to sign and verify payloads. ## Sign with an external wallet When a user creates, adds, removes, or revokes an XMTP inbox's identity or installation, a signature is required. ## Sign with an XMTP key You can sign something with XMTP keys. For example, you can sign with XMTP keys to send a payload to a backend. :::code-group ```js [Node] const signature = client.signWithInstallationKey(signatureText); ``` ```jsx [React Native] const signature = await client.signWithInstallationKey(signatureText); ``` ```kotlin [Kotlin] val signature = client.signWithInstallationKey(signatureText) ``` ```swift [Swift] let signature = try client.signWithInstallationKey(message: signatureText) ``` ::: ## Verify with the same installation that signed You can also sign with XMTP keys and verify that a payload was sent by the same client. :::code-group ```js [Node] const isValidSignature = client.verifySignedWithInstallationKey( signatureText, signature ); ``` ```jsx [React Native] const isVerified = await client.verifySignature(signatureText, signature); ``` ```kotlin [Kotlin] val isVerified = client.verifySignature(signatureText, signature) ``` ```swift [Swift] let isVerified = try client.verifySignature( message: signatureText, signature: signature ) ``` ::: ## Verify with the same inbox ID that signed You can use an XMTP key's `installationId` to create a signature, then pass both the signature and `installationId` to another `installationId` with the same `inboxId` to verify that the signature came from a trusted sender. :::code-group ```js [Node] const isValidSignature = client.verifySignedWithPrivateKey( signatureText, signature, installationId ); ``` ```kotlin [Kotlin] val isVerified = client.verifySignatureWithInstallationId( signatureText, signature, installationId ) ``` ```swift [Swift] let isVerified = try client.verifySignatureWithInstallationId( message: signatureText, signature: signature, installationId: installationId ) ``` ::: ## chat-apps/push-notifs/android-pn.mdx # Try push notifications with the Android example XMTP app This guide describes how to set up push notifications for the [XMTP Android example app](https://github.com/xmtp/xmtp-android/tree/main/example) built with the [xmtp-android SDK](https://github.com/xmtp/xmtp-android) using Firebase Cloud Messaging (FCM) and a custom notification server. Perform this setup to understand how you can enable push notifications for your own app built with the `xmtp-android` SDK. ## Set up a Firebase Cloud Messaging server For this tutorial, we'll use [Firebase Cloud Messaging](https://console.firebase.google.com/) (FCM) as a convenient way to set up a messaging server. 1. Create an FCM project. 2. Add the example app to the FCM project. This generates a `google-services.json` file that you need in subsequent steps. 3. Add the `google-services.json` file to the example app's project as described in the FCM project creation process. 4. Generate FCM credentials, which you need to run the example notification server. To do this, from the FCM dashboard, click the gear icon next to **Project Overview** and select **Project settings**. Select **Service accounts**. Select **Go** and click **Generate new private key**. ## Run an example notification server Now that you have an FCM server set up, take a look at the `kotlin` folder in the `example-notifications-server-go` repo. These files can serve as the basis for what you might want to provide for your own notification server. This proto code from the example notification server has already been generated and added to the `xmtp-android` example app if you use the example notification server as-is. **To run an example notification server:** 1. Clone the [example-notification-server-go](https://github.com/xmtp/example-notification-server-go) repo. 2. Complete the steps in [Local Setup](https://github.com/xmtp/example-notification-server-go/blob/np/export-kotlin-proto-code/README.md#local-setup). 3. Get the FCM project ID and the FCM credentials you created in step 4 of setting up FCM and run: ```bash [Bash] YOURFCMJSON=$(cat /path/to/FCMCredentials.json) ``` ```bash [Bash] dev/run \ --xmtp-listener-tls \ --xmtp-listener \ --api \ -x "grpc.production.xmtp.network:443" \ -d "postgres://postgres:xmtp@localhost:25432/postgres?sslmode=disable" \ --fcm-enabled \ --fcm-credentials-json=$YOURFCMJSON \ --fcm-project-id="YOURFCMPROJECTID" ``` 4. You should now be able to see push notifications coming across the local network. ## Update the example app to send push notifications 1. Add your `google-services.json` file to the `example` folder, if you haven't already done it as a part of the FCM project creation process. 2. Uncomment `id 'com.google.gms.google-services'` in the example app's `build.gradle` file. 3. Uncomment the following code in the top level of the example app's `build.gradle` file: ```groovy [Groovy] buildscript { repositories { google() mavenCentral() } dependencies { classpath 'com.google.gms:google-services:4.3.15' } } ``` 4. Sync the Gradle project. 5. Add the example notification server address to the example app's `MainActivity`. In this case, it should be `PushNotificationTokenManager.init(this, "10.0.2.2:8080")`. 6. Change the example app's environment to `XMTPEnvironment.PRODUCTION` in `ClientManager.kt`. 7. Set up the example app to register the FCM token with the network and then subscribe each conversation to push notifications. For example: ```kotlin [Kotlin] XMTPPush(context, "10.0.2.2:8080").register(token) ``` ```kotlin [Kotlin] val hmacKeysResult = ClientManager.client.conversations.getHmacKeys() val subscriptions = conversations.map { val hmacKeys = hmacKeysResult.hmacKeysMap val result = hmacKeys[it.topic]?.valuesList?.map { hmacKey -> Service.Subscription.HmacKey.newBuilder().also { sub_key -> sub_key.key = hmacKey.hmacKey sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch }.build() } Service.Subscription.newBuilder().also { sub -> sub.addAllHmacKeys(result) sub.topic = it.topic sub.isSilent = it.version == Conversation.Version.V1 }.build() } XMTPPush(context, "10.0.2.2:8080").subscribeWithMetadata(subscriptions) ``` ```kotlin [Kotlin] XMTPPush(context, "10.0.2.2:8080").unsubscribe(conversations.map { it.topic }) ``` ## Decode a notification envelope You can decode a single `Envelope` from XMTP using the `decode` method: ```kotlin [Kotlin] val conversation = client.conversations.newConversation("0x3F11b27F323b62B159D2642964fa27C46C841897") // Assume this function returns an Envelope that contains a message for the above conversation val envelope = getEnvelopeFromXMTP() val decodedMessage = conversation.decode(envelope) ``` ## chat-apps/push-notifs/ios-pn.mdx # Try push notifications with the iOS example XMTP app This guide describes how to set up push notifications for the [XMTP iOS example app](https://github.com/xmtp/xmtp-ios/tree/main/XMTPiOSExample) built with the [xmtp-ios SDK](https://github.com/xmtp/xmtp-ios) using Firebase Cloud Messaging (FCM) and a custom notification server. Perform this setup to understand how you can enable push notifications for your own app built with the `xmtp-ios` SDK. ## Prerequisites - An iOS device for testing. Push notifications don't work on simulators - A Firebase account and a project set up in the Firebase console ## Set up Firebase Cloud Messaging For this tutorial, we'll use [Firebase Cloud Messaging](https://console.firebase.google.com/) (FCM) as a convenient way to set up a messaging server. 1. Create an FCM project Go to the [Firebase Console](https://console.firebase.google.com/), create a new project, and follow the setup instructions. 2. Add your app to the FCM project Add your iOS app to the project by following the Firebase setup workflow. You'll need your app's bundle ID. 3. Download `GoogleService-Info.plist` At the end of the setup, download the `GoogleService-Info.plist` file and add it to your Xcode project. 4. Generate FCM credentials In the Firebase console, navigate to your project settings, select the **Cloud Messaging** tab, and note your server key and sender ID. You'll need these for your notification server. ## Configure the iOS example app for push notifications 1. Enable push notifications In Xcode, go to your project's target capabilities and enable push notifications. 2. Register for notifications Modify the `AppDelegate` to register for remote notifications and handle the device token. 3. Handle incoming notifications Implement the necessary delegate methods to handle incoming notifications and foreground notification display. ## Run the notification server 1. Clone and configure the notification server If you're using the example notification server, clone the repository and follow the setup instructions. Make sure to configure it with your FCM server key. 2. Run the server Start the server locally or deploy it to a hosting service. - Subscribe to push notifications in the app When initializing the XMTP client in your app, subscribe to push notifications using the device token obtained during registration. - Decode a notification envelope When you receive a push notification, you may want to decode the notification envelope to display a message preview or other information. ## chat-apps/push-notifs/pn-server.mdx # Run a push notification server for an app built with XMTP This guide supplements the [core instructions](https://github.com/xmtp/example-notification-server-go/blob/np/export-kotlin-proto-code/README.md#local-setup) provided in the `example-notification-server-go` repository. The guide aims to address some common setup misconceptions and issues. This guide is for macOS users, but the steps should be similar for Linux users. ## Useful resources - [Notification server example implementation](https://github.com/xmtp/example-notification-server-go) - [Notification server integration test suite](https://github.com/xmtp/example-notification-server-go/blob/main/integration/README.md) ## Install Docker 1. Install Docker Desktop: - [Mac](https://docs.docker.com/docker-for-mac/install/) - [Windows](https://docs.docker.com/docker-for-windows/install/) - [Linux](https://docs.docker.com/desktop/install/linux-install/) 2. You must also have Docker and Docker Compose installed on your system. You can install them using Homebrew: ```bash [Bash] brew install docker docker-compose docker-credential-desktop ``` 3. Make sure Docker Desktop is running by searching for Docker in Spotlight and opening the application. You don't need to interact with the Docker UI. We're going to use terminal commands only. ## Install Go If you need to upgrade Go on your system, you can do it via Homebrew: ```bash [Bash] brew install go ``` If you encounter an error like `Error: go 1.17.2 is already installed`, you already have Go installed on your system. You can check the version of Go installed on your system using: ```bash [Bash] brew update brew upgrade go ``` After following these steps, you can update Homebrew and upgrade Go without issues. ## Set up the server 1. To start the XMTP service and database, navigate to the project terminal and run: ```bash [Bash] ./dev/up ``` ![set up the server](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/cmd1.png) If you encounter an error like `error getting credentials - err: docker-credential-desktop resolves to executable in current directory (./docker-credential-desktop), out:`, it's likely because Docker is not running. Make sure Docker Desktop is running and try the command again. 2. Build the server using: ```bash [Bash] ./dev/build ``` During the build, if you encounter any Go-related errors like missing `go.sum` entries, use the suggested `go get` or `go mod download` commands in the error messages to resolve them. For example, if you see `missing go.sum entry; to add it: go mod download golang.org/x/sys`, run: ```bash [Bash] go mod download golang.org/x/sys ``` If you encounter errors related to Go build comments like `//go:build comment without // +build comment`, you can ignore them as they are warnings about future deprecations and won't prevent your code from running. ## Run the server Run the server using the `./dev/run` script with the `--xmtp-listener` and `--api` flags: ```bash [Bash] ./dev/up ``` ![./dev/up in CLI](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/cmd2.png) ```bash [Bash] source .env ./dev/run --xmtp-listener --api ``` This starts both the `worker` and `api` services. The `worker` listens for new messages on the XMTP network and sends push notifications. The `api` service handles HTTP/GRPC requests. ![./dev/run --xmtp-listener --api in CLI](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/cmd3.png) You can now send notifications to your device using an [XMTP push notification client](https://github.com/xmtp/example-notification-server-go/blob/main/docs/notifications-client-guide.md). ![dev/run in CLI](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/cmd4.png) ## Troubleshooting - If Docker or Docker Compose commands aren't recognized, it might mean that they aren't installed or their executable paths aren't included in your system's PATH variable. Make sure Docker and Docker Compose are installed and their paths are included in your system's PATH. - If you encounter Go-related errors during the build, it's often due to missing packages or outdated versions. Make sure your Go is up to date and use the `go get` or `go mod download` commands to fetch the necessary dependencies. - If you encounter any error like `./dev/up: line 3: docker-compose: command not found`, it's because you don't have Docker Compose installed on your system. Use the above command to install it. - If you see warnings about `//go:build comment without // +build comment`, these are warnings about future deprecations in Go. They won't prevent your code from running and can be ignored. - If `brew update` gives errors, it might be due to changes in Homebrew's repository. Homebrew switched to using `main` as its default branch. The steps provided in the "Upgrading Go" section should help resolve this issue. - If you encounter any errors during `brew update`, such as `fatal: couldn't find remote ref refs/heads/master`, Homebrew is having trouble updating its repositories. To fix this, run: ```bash [Bash] cd $(brew --repository) git checkout main git reset --hard origin/main ``` - Here is a piece of code that points to the ports and network. Be sure to use TLS like this `./dev/run --xmtp-listener-tls --api`. :::code-group ```tsx [Browser] export const ApiUrls = { local: 'http://localhost:5555', dev: 'https://dev.xmtp.network', production: 'https://production.xmtp.network', } as const; export const HistorySyncUrls = { local: 'http://localhost:5558', dev: 'https://message-history.dev.ephemera.network', production: 'https://message-history.production.ephemera.network', } as const; ``` ```tsx [Node] export const ApiUrls = { local: 'http://localhost:5556', dev: 'https://grpc.dev.xmtp.network:443', production: 'https://grpc.production.xmtp.network:443', } as const; ``` ```tsx [React Native] const ApiUrls = { local: 'http://localhost:5556', dev: 'https://grpc.dev.xmtp.network:443', production: 'https://grpc.production.xmtp.network:443', }; ``` ```kotlin [Kotlin] enum ApiUrls { static let local = "http://localhost:5556" static let dev = "https://grpc.dev.xmtp.network:443" static let production = "https://grpc.production.xmtp.network:443" } ``` ```swift [Swift] object ApiUrls { const val local = "http://localhost:5556" const val dev = "https://grpc.dev.xmtp.network:443" const val production = "https://grpc.production.xmtp.network:443" } ``` ::: ## chat-apps/push-notifs/push-notifs.mdx # Support push notifications With XMTP, you can enable real-time push notifications to keep users updated on new conversations and messages. ## Get a Welcome message topic ID Get the topic identifier for an app installation. This topic ID tells your app where to listen on the network for push notifications about any new group chat or DM conversations. :::code-group ```tsx [React Native] // Request alix.welcomeTopic() // Response /xmtp/mls/1/w-$installationId/proto ``` ```kotlin [Kotlin] // Request Topic.userWelcome(client.installationId).description // Response /xmtp/mls/1/w-$installationId/proto ``` ```swift [Swift] // Request Topic.userWelcome(client.installationId).description // Response /xmtp/mls/1/w-$installationId/proto ``` ::: ## Get a message topic ID Get the topic identifier for a group chat or DM conversation that's already in progress. This topic ID tells your app where to listen on the network for push notifications about any new messages in a specific group chat or DM conversation. :::code-group ```tsx [React Native] // Request conversation.topic // Response /xmtp/mls/1/g-$conversationId/proto ``` ```kotlin [Kotlin] // Request conversation.topic // Response /xmtp/mls/1/g-$conversationId/proto ``` ```swift [Swift] // Request conversation.topic // Response /xmtp/mls/1/g-$conversationId/proto ``` ::: ## List push topics for a DM conversation Lists topics for a specific DM conversation. :::code-group ```tsx [React Native] conversation.getPushTopics() ``` ```kotlin [Kotlin] conversation.getPushTopics() ``` ```swift [Swift] conversation.getPushTopics() ``` ::: ## List push topics for all conversations Lists topics for all group chat and DM conversations, including duplicate DM conversations. Duplicate DM conversations can occur when a user creates new conversations with the same contact from different installations or devices. While DM stitching automatically combines these conversations into a single view for users, each duplicate conversation maintains its own push notification topic. Ensure that users receive push notifications for messages sent through any of the stitched conversations, even as the system gradually consolidates them into a single conversation. To learn more, see [Understand DM stitching and push notifications](/chat-apps/push-notifs/understand-push-notifs#understand-dm-stitching-and-push-notifications). :::code-group ```tsx [React Native] conversations.allPushTopics() ``` ```kotlin [Kotlin] conversations.allPushTopics() ``` ```swift [Swift] conversations.allPushTopics() ``` ::: ## Subscribe to topics Subscribe to all relevant topics, allowing your app to monitor for push notifications about both new and ongoing conversations. This code sample retrieves all topics associated with `alix`'s conversations, for example, enabling the app to receive push notifications only for conversations in which `alix` is a part of. :::code-group ```tsx [React Native] const conversations = await alix.conversations.list(); const topics = conversations.map((conv: any) => conv.topic); await subscribeAll([alix.welcomeTopic(), ...topics]); ``` ```kotlin [Kotlin] val conversations = alix.conversations.list() val topics = conversations.map { it.topic }.toMutableList() subscribeAll(topics.push(Topic.userWelcome(client.installationId).description)) ``` ```swift [Swift] let conversations = try await alix.conversations.list() var topics = conversations.map { $0.topic } subscribeAll(topics.append(Topic.userWelcome(client.installationId).description))``` ::: ## Receive push notifications On receipt of a push notification, decode it: :::code-group ```tsx [React Native] const receivedBytes = Buffer.from(received.message, "base64").toString("utf-8"); ``` ```kotlin [Kotlin] val encryptedMessage = remoteMessage.data["encryptedMessage"] val encryptedMessageData = Base64.decode(encryptedMessage, Base64.NO_WRAP) ``` ```swift [Swift] let encryptedMessage = remoteMessage.data["encryptedMessage"] let encryptedMessageData = Data(base64Encoded: encryptedMessage) ``` ::: Then determine whether it's for a new conversation or an existing one. - **If it's a Welcome message for a new conversation** (`alix.welcomeTopic() == received.topic`), initiate the conversation with `fromWelcome`: :::code-group ```tsx [React Native] const conversation = await alix.conversations.fromWelcome( receivedBytes ); ``` ```kotlin [Kotlin] val conversation = alix.conversations.fromWelcome(receivedBytes) ``` ```swift [Swift] let conversation = try await alix.conversations.fromWelcome(receivedBytes) ``` ::: - **If it's a message for an existing conversation**, find the corresponding topic, sync the conversation, and process the new message: :::code-group ```tsx [React Native] const conversation = await alix.findConversationByTopic(received.topic); await conversation.sync(); const message = await conversation.processMessage(receivedBytes); ``` ```kotlin [Kotlin] val conversation = alix.findConversationByTopic(received.topic) conversation.sync() val message = conversation.processMessage(receivedBytes) ``` ```swift [Swift] let conversation = try alix.findConversationByTopic(received.topic) try await conversation.sync() let message = try await conversation.processMessage(receivedBytes) ``` ::: ## Stream HMAC key update events Use `preferences.stream` to listen for events that indicate when a new HMAC key has been added, typically due to a new installation joining the network. This stream does not provide the actual HMAC keys but notifies you to refresh your HMAC keys accordingly by resubscribing to topics. This is particularly useful for ensuring that push notifications work correctly across all installations by not missing updates from new installations. :::code-group ```tsx [React Native] await alix.preferences.stream((event: any) => { // Received a preference update event // Check if it indicates a new HMAC key and resubscribe if necessary }); ``` ```kotlin [Kotlin] alix.preferences.stream().collect { event -> // Received a preference update event } ``` ```swift [Swift] for await event in try await alix.preferences.stream() { // Received a preference update event } ``` ::: ## Resubscribe to topics to get new HMAC keys As soon as your app receives a user preference update event from [preferences.sync](/chat-apps/list-stream-sync/sync-preferences) or [preferences.stream](#stream-hmac-key-update-events) indicating new HMAC keys for a user, resubscribe to topics to get the new HMAC keys. For example: ```kotlin [Kotlin] conversations.allTopics.forEach { -> topic val hmacKeysResult = conversations.getHmacKeys() val hmacKeys = hmacKeysResult.hmacKeysMap val result = hmacKeys[topic]?.valuesList?.map { hmacKey -> Service.Subscription.HmacKey.newBuilder().also { sub_key -> sub_key.key = hmacKey.hmacKey sub_key.thirtyDayPeriodsSinceEpoch = hmacKey.thirtyDayPeriodsSinceEpoch }.build() } val subscription = Service.Subscription.newBuilder().also { sub -> sub.addAllHmacKeys(result) sub.topic = topic sub.isSilent = false }.build() } PushNotificationTokenManager.xmtpPush.subscribeWithMetadata(subscription) ``` This ensures that older installations (or your XMTP push notification server code) now know about and resubscribe to all conversations for all of the new HMAC keys. ## Get HMAC keys for a conversation Gets the HMAC keys for a specific group chat or DM conversation. For DM conversations, the response includes HMAC keys for all topics associated with the conversation. This is necessary because a single DM conversation can have multiple underlying topics due to DM stitching, where multiple conversations between the same participants are combined into one view. For example, if Alix and Bo have a conversation that was created from two different installations, there might be two topics (`topic1` and `topic2`) for what appears as a single conversation to the user. The response will include HMAC keys for both topics to ensure push notifications work correctly regardless of which topic the message was sent through. To learn more, see [Understand DM stitching and push notifications](/chat-apps/push-notifs/understand-push-notifs#understand-dm-stitching-and-push-notifications). :::code-group ```tsx [React Native] conversation.getHmacKeys() ``` ```kotlin [Kotlin] conversation.getHmacKeys() ``` ```swift [Swift] conversation.getHmacKeys() ``` ::: ## Run a push notification server To implement push notifications in your app, you must run a push notification server. This server acts as an intermediary between the XMTP network and your app's push notification service, ensuring that users receive timely and relevant notifications. ### Why is a push notification server required? - **Continuous monitoring**: The XMTP network operates on a decentralized protocol, where messages are exchanged directly between clients. However, for push notifications, your app needs a dedicated server to continuously listen for new messages or events on the XMTP network, even when the user's device is offline or the app is not running. - **Message processing**: Upon detecting new messages or events, the server processes them to determine their relevance and formats them appropriately for push notifications. This includes extracting necessary information, such as the sender's identity and message content, to craft meaningful notifications. - **Integration with push notification services**: The server interfaces with platform-specific push notification services, like [Apple Push Notification Service](/chat-apps/push-notifs/understand-push-notifs#best-practices-for-apple-push-notifications) (APNs) for iOS or Firebase Cloud Messaging (FCM) for Android. It sends the processed notifications to these services, which then deliver them to the user's device. To learn more about running a push notification server, see [Understand push notifications with XMTP](/chat-apps/push-notifs/understand-push-notifs). Then you can: - [Set up a Go push notification server](/chat-apps/push-notifs/pn-server) - [Try push notifications with the Android example XMTP app](/chat-apps/push-notifs/android-pn) - [Try push notifications with the iOS example XMTP app](/chat-apps/push-notifs/ios-pn) ## chat-apps/push-notifs/understand-push-notifs.mdx # Understand push notifications with XMTP With XMTP, you can enable real-time push notifications to keep users updated on new conversations and messages. At the highest level, push notifications with XMTP require these three elements: 1. An **XMTP push notification server** that listens for all messages sent on the XMTP network. You set the server to listen to the`production`, `dev`, or `local` environment, and every message sent using that environment flows through the server. The server filters the messages accordingly and sends only the desired push notifications to a push notification service. 2. A **push notification service**, such as Apple Push Notification service (APNs), Firebase Cloud Messaging (FCM), or W3C Web Push protocol, receives push notifications from the XMTP push notification server. 3. An **app** that displays the push notifications. ![Push notifications framework](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/pn-framework.png) ## Understand message filtering Let's dive deeper into how the XMTP push notification server filters messages to determine which ones to send to the push notification service. ![XMTP push notification server filtering flow](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/pn-server-filtering.png) 1. **Check if the server is subscribed to the message's topic** - A topic is a way to organize messages, and each message has a topic. To support push notifications, your app must [subscribe the server to the topics](https://docs.xmtp.org/chat-apps/push-notifs/push-notifs#subscribe-to-topics) that are relevant to your users. For example, for a user Alix, you must subscribe to all topics associated with Alix's conversations. The XMTP push notification server has a list of these subscriptions. Your push notification server should expose functions to post the subscriptions to. The SDKs use protobufs as a universal language that allows the creation of these functions in any language. For example, [here are bufs](https://github.com/xmtp/xmtp-android/tree/main/library/src/main/java/org/xmtp/android/library/push) generated from the [XMTP example push notification server](/chat-apps/push-notifs/pn-server). You can use these directly if you clone and use the example server. - If the arriving message's topic is **not on the list**, the server ignores it. - If the arriving message's topic is **on the list**, the server proceeds to check the message with the next filter. 2. **Check the `shouldPush` field value** - Each [content type](/chat-apps/content-types/content-types), such as text, attachment, or reply, can have - A `shouldPush` boolean value is set at the content type level for each content type, such as text, attachment, or reply, so it can't be overwritten on send. By default, this value is set to **_true_** for all content types except read receipts and reactions. - If the message's content type `shouldPush` value is **_false_**, the server ignores the message. - If the message's content type `shouldPush` value is **_true_**, the server proceeds to check the message with the next filter. 3. **Check the message's HMAC key** - Each message sent with XMTP carries a single HMAC key. This key is updated with the encrypted message payload before being sent out. - If the message is signed by an HMAC key that **matches** the user's HMAC key, the push notification server ignores the message. This match means that the message was sent by the user themself, and they should not receive a push notification about a message they sent. - If the message is signed by an HMAC key that **does not match** the user's HMAC key, this means someone else sent the message and the user should be notified about it. At this point, the push notification server will send a notification. 4. **Send to the push notification service** - The server sends the message to the push notification service. - Once the push notification service has the message, it can format it appropriately for the push notification. This includes extracting necessary information, such as the sender's identity and message content, to craft meaningful notifications. This is only possible with the push notifications service inside the app and not with the push notification server because the server doesn't have the notion of a client and, therefore, can't decrypt the message. XMTP provides an example XMTP push notification server that implements the filtering described here. To learn more, see [Run a push notification server for an app built with XMTP](/chat-apps/push-notifs/pn-server). ## Understand sending push notifications The XMTP push notification server sends qualified messages to the appropriate push notification service: - Apple Push Notification service (APNs) for iOS apps - Firebase Cloud Messaging (FCM) for Android apps - W3C Web Push protocol for web apps ### Best practices for Apple push notifications While we recommend that you do message filtering at the XMTP push notification server level, if you build an iOS app with XMTP, you can choose to use the [com.apple.developer.usernotifications.filtering](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_usernotifications_filtering) entitlement instead. This entitlement enables you to run a Notification Service Extension in your app that can decrypt and modify a notification before displaying it to a user. To use this entitlement, you must obtain permission from Apple. Submit the application using the app owner's Apple developer account via [https://developer.apple.com/contact/request/notification-service](https://developer.apple.com/contact/request/notification-service). :::warning Keep in mind that it may be difficult to obtain permission for the `com.apple.developer.usernotifications.filtering` entitlement from Apple if your app handles cryptocurrency or could be perceived—even loosely—as a crypto wallet. While Apple hasn't publicly stated an explicit policy, developers have anecdotally reported challenges in this area. If you don't receive the entitlement, you can always perform the message filtering using the XMTP push notification server. ::: **Submit your application early** The approval process can take 2-3 weeks or longer. Here are some sample answers to help you complete the application: - **App name:** [Your app name] - **App Store URL:** [Your app store URL] - **Apple ID of App:** [Your app ID] - **App Type:** Messaging - **Does your app provide end-to-end encryption?:** Yes - **Explain why existing APIs are not adequate for your app:** The existing APIs always show some sort of notification when a push comes in. We don't want to show a notification for a user's own messages. - **Explain why your app doesn't show a visible notification each time a push notification is received:** The server delivering notifications only knows of the existence of a conversation. It does not know who the sender or recipient is. That data is decoded on the device in the extension. As a result, it sends a push notification for every message that occurs in the conversation. We want to filter out notifications for notifications that the user themself sent. - **When your extension runs, what system and network resources does it need:** We might need to make a gRPC request to load additional information about a conversation. This is only necessary when we haven't stored the conversation details locally, which is expected to be less common than being able to just decrypt the conversation locally. For example, we might have a scenario in which a welcome message is pushed before it's streamed. - **How often does your extension run? What can trigger it to run:** The extension will run whenever a message is sent or received in a conversation. The frequency will depend on how active a user is. ## Understand displaying push notifications After the push notification service sends the notification, the app must [receive it](/chat-apps/push-notifs/push-notifs#receive-push-notifications). There are some nuances to how push notifications can be handled once received by the app. While it is useful for all of these app types to use the XMTP push notification server filtering capabilities, it is especially important for an iOS app without the user notification entitlement. | | Can decrypt before displaying? | | --------------------------------------------- | ------------------------------ | | Android and web apps | Yes | | iOS app with user notification entitlement | Yes | | iOS app without user notification entitlement | No | - If the app **can** **decrypt** the push notification before displaying it to the user, it can perform additional logic (should I display this?) before displaying the push notification. For example, the app can decrypt the push notification, see the topic type, and process it accordingly: - Is the message in a welcome topic? - `conversations.processWelcomeMessage` - Is the message in a conversation topic? - `conversation.processMessage` - If the app **cannot** **decrypt** the push notification before displaying it to the user, it can't perform additional logic (should I display this?) before displaying the push notification. For example, without the user notification entitlement, the app cannot decrypt and modify the push notification before displaying it to the user. The notification arrives on the device, and iOS handles displaying it automatically. You can decrypt the content after the notification is shown, but you cannot intercept it before display and decide not to show it, for example. ## Understand HMAC keys and push notifications XMTP uses Hash-based Message Authentication Code (HMAC) keys for push notifications. A user holds the HMAC keys for any conversation they join, but an outside observer only sees the keys without knowing who owns them. For instance, suppose Alix has HMAC key #1, and we also see HMAC keys #2 and #3. If Alix discloses that they hold key #1, then we know key #1 belongs to them. However, we have no way of knowing who holds keys #2 or #3 unless those individuals reveal that information. This design preserves privacy while enabling secure communication. ### HMAC key generation and syncing XMTP uses a two-level HMAC key system: 1. **Root HMAC key**: Each inbox has a root HMAC key that serves as the foundation for deriving group-specific keys 2. **Group HMAC keys**: Each conversation group has its own HMAC key, derived from the root HMAC key, the message's group ID, and the number of 30-day periods since the Unix epoch, along with some salt. XMTP generates one HMAC key per group, not per installation. #### Root HMAC key syncing across installations When a new installation for an inbox is created, it generates a new root HMAC key and publishes it to the sync group. Other installations of the same inbox will eventually replace their root HMAC keys with the last one posted in the sync group. This means the root HMAC key eventually becomes the same across all installations for a given inbox. #### Effects of stale root HMAC keys The only side effect of having a stale root HMAC key is that a user might receive push notifications for their own messages. This happens because the push server uses group HMAC keys (derived from the root key) to filter out a user's own messages. When the derived group keys don't match due to having a stale root key, the server can't identify that the user sent the message and sends them a notification. #### How installations learn about new root HMAC keys This is one of the jobs of the [history sync](/chat-apps/list-stream-sync/history-sync) feature. It listens for `preferences.streamAllPreferenceUpdates()`, which are user preferences that may include an enum with root HMAC keys for new installations. When a user's new installation publishes updated root HMAC key info, older installations can see that update and must [resubscribe to topics](/chat-apps/push-notifs/push-notifs#resubscribe-to-topics-to-get-new-hmac-keys) to get the new group HMAC keys derived from the updated root key. ## Understand DM stitching and push notifications 🎥 walkthrough: DM stitching This video provides a walkthrough of direct message (DM) stitching, covering the key ideas discussed in this section. After watching, feel free to continue reading for more details. Consider a scenario where `alix.id` using an existing installation #1 to create a conversation with `bo.id` and sends them a DM. And then `alix.id` creates a new installation #2, and instead of waiting for [history sync](https://docs.xmtp.org/chat-apps/list-stream-sync/history-sync) to bring in their existing conversations, `alix.id` creates a new conversation with `bo.id` and sends them a DM. Under the hood, this results in two DM conversations (or two MLS groups) with the same pair of identities, `alix.id` and `bo.id`, resulting in a confusing DM UX like this one: ![No DM stitching](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/no-dm-stitching.png) XMTP implements DM stitching to ensure that even if there are multiple DMs with the same pair of identities under the hood, your users see only one DM conversation with messages displayed appropriately. For example, with DM stitching, instead of seeing two separate DM conversations between `alix.id` and `bo.id` with one message each, `alix.id` sees one DM conversation between `alix.id` and `bo.id` with two messages in both installations #1 and #2 ![With DM stitching](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/with-dm-stitching.png) ### How DM stitching works 1. When fetching messages from any of the MLS groups associated with a DM conversation, the XMTP SDK responds with messages from all of the groups associated with the DM conversation. - For example, let's say you have three MLS groups associated with a DM conversation: - alix-bo-1 - alix-bo-2 - alix-bo-3 Any messages sent in any of these DM conversations will display in all of these DM conversations. For example, `alix.id` sends a message in alix-bo-1. `bo.id` is on alix-bo-3, but can still see messages sent in alix-bo-1. 2. When sending messages in a DM conversation, all installations in that DM will eventually converge to sending them to the same MLS group, even if they originally start off using different ones. - For example, `bo.id` sends a message in alix-bo-3. `alix.id` is on alix-bo-1, but can still see messages from alix-bo-3. When `alix.id` sends a reply to `bo.id`, it uses the most recently used DM conversation, alix-bo-3. In this way, all messaging will eventually move to alix-bo-3, and 1 and 2 will slowly fade away due to non-use. ### DM stitching considerations for push notifications DM stitching provides a unified UX in the app. However, the multiple DM conversations under the hood must still be addressed for push notifications. Let's take DM conversations alix-bo-1 and alix-bo-3 between `alix.id` and `bo.id`. With DM stitching, these two conversations display as one conversation. However, we must remember that they have two different conversation IDs, and thus two different topics. Therefore, when subscribing a DM conversations to push notifications, you should subscribe to a list of topics because every DM conversation can potentially have multiple topics. You will miss push notifications for messages if you are not listening to every potential topic for a DM conversation that messages could potentially be sent on. For example, you must consider that with DM stitching, the conversation is moving from alix-bo-1 and alix-bo-3, for example, and [resubscribe appropriately](/chat-apps/push-notifs/push-notifs#resubscribe-to-topics-to-get-new-hmac-keys). Also, you must consider that a welcome message will be sent when a DM conversation is added to the stitched DMs, and you should not send a push for the welcome message because the user already has a conversation with the person. It is just a different DM conversation in the set of DM conversations that are stitched together. These welcome messages are filtered out of streams, but they are not filtered out for the XMTP push notification server, so you must handle these duplicate DM welcomes at the push notification service. ## chat-apps/list-stream-sync/archive-backups.md # Support archive-based backups Support archive-based backups to give your users an easy and durable way to back up their XMTP conversations, messages, and consent preferences from one app installation and import them into another. For example, a user can set up an archive-based backup for an app installation on their device. If they lose their device and get a new one, they can download and import their archive-based backup onto their new device. When an archive-based backup is imported into an installation, **all imported conversations start off as inactive, with history visible and in read-only mode**. This is as intended by Messaging Layer Security (MLS), which ensures that there is no way to join a conversation without the permission of an existing member. As such, imported conversations will be inactive until the new installation is added to the conversation by an active member. This process happens invisibly and automatically as other members of the group come online and send or receive messages. To learn more, see [4. Handle post-import conversation statuses](#4-handle-post-import-conversation-statuses). This feature includes three core methods: - `createArchive(path, encryptionKey, options?)` - `archiveMetadata(path, encryptionKey)` - `importArchive(path, encryptionKey)` ## 1. Create the archive To enable a user to create an archive: 1. Specify the archive file path (e.g., iCloud, Google Cloud, or your server). Ensure the parent folder already exists. 2. Generate a 32-byte array encryption key to protect the archive contents. This ensures that other apps and devices cannot access the contents without the key. Securely store the key in a location that is secure, independent of the archive file, and will persist after your app has been uninstalled. A common place to store this encryption key is the iCloud Keychain. 3. Call `createArchive(path, encryptionKey, options?)` with the archive file path and the encryption key. Optionally, you can pass in the following: - Archive start and end time. If left blank, the archive will include all time. - Archive contents, which can be `Consent` or `Messages`. If left blank, the archive will include both. :::code-group ```tsx [React Native] createArchive(path: string, encryptionKey: string | Uint8Array, options?: { startTime?: Date, endTime?: Date, elements?: ("Consent" | "Messages")[] }) ``` ```kotlin [Kotlin] // Create an archive backup XMTPClient.createArchive( path = "/path/to/archive.xmtp", encryptionKey = encryptionKey, options = ArchiveOptions( startTime = startTime, endTime = endTime, elements = listOf(ArchiveElement.CONSENT, ArchiveElement.MESSAGES) ) ) ``` ```swift [Swift] // Create an archive backup try await xmtp.createArchive( path: "/path/to/archive.xmtp", encryptionKey: encryptionKey, options: ArchiveOptions( startTime: startTime, endTime: endTime, elements: [.consent, .messages] ) ) ``` ::: This writes the selected content into the specified file location and encrypts it using the provided key. If the user tries to close the app before `createArchive` is complete, you can do a check to see if the file on the server is empty. If empty, display a warning to the user letting them know that exiting the app will cancel archive creation. ## 2. Check archive metadata To enable a user to view information about their archive(s) before importing it to an app installation: :::code-group ```tsx [React Native] archiveMetadata(path: string, encryptionKey: string) ``` ```kotlin [Kotlin] // Get archive metadata val metadata = XMTPClient.archiveMetadata( path = "/path/to/archive.xmtp", encryptionKey = encryptionKey ) // metadata.startTime, metadata.endTime, metadata.elements, metadata.createdAt ``` ```swift [Swift] // Get archive metadata let metadata = try await xmtp.archiveMetadata( path: "/path/to/archive.xmtp", encryptionKey: encryptionKey ) // metadata.startTime, metadata.endTime, metadata.elements, metadata.createdAt ``` ::: This will return information that enables the user to better understand the archive(s) they want to import: - Start and end time of archived data - Archived elements (messages and/or consent) - Archive creation date You can get the archive file size from the file system. ## 3. Import the archive To enable a user to import a selected archive to an installation: :::code-group ```tsx [React Native] importArchive(path: string, encryptionKey: string) ``` ```kotlin [Kotlin] // Import an archive backup XMTPClient.importArchive( path = "/path/to/archive.xmtp", encryptionKey = encryptionKey ) ``` ```swift [Swift] // Import an archive backup try await xmtp.importArchive( path: "/path/to/archive.xmtp", encryptionKey: encryptionKey ) ``` ::: This downloads and integrates the archive data into the app installation’s local database. The archive import is **additive**, not destructive: existing messages are preserved, and duplicate messages are ignored. If the user tries to close the app before `importArchive` is complete, display a warning to the user letting them know that exiting the app will cancel the archive import. ## 4. Handle post-import conversation statuses After importing the archive to an app installation, **all imported conversations will be inactive, with history visible and in read-only mode**, as intended by MLS as described earlier. You should gray out UI functionality that involves writing to or modifying inactive conversations. Attempting to send or sync on inactive conversations will throw a `Group is inactive` error. To check conversation status before initiating a network action: :::code-group ```js [Browser] // Check if the conversation is active (async method in Browser SDK) if (await conversation.isActive()) { // Conversation is active, safe to send or sync } else { // Conversation is inactive, show read-only UI } ``` ```js [Node] // Check if the conversation is active (async method in Node SDK) if (await conversation.isActive()) { // Conversation is active, safe to send or sync } else { // Conversation is inactive, show read-only UI } ``` ```tsx [React Native] conversation.isActive(); ``` ```kotlin [Kotlin] // Check if the conversation is active if (conversation.isActive()) { // Conversation is active, safe to send or sync } else { // Conversation is inactive, show read-only UI } ``` ```swift [Swift] // Check if the conversation is active if conversation.isActive() { // Conversation is active, safe to send or sync } else { // Conversation is inactive, show read-only UI } ``` ::: This will check to see if the installation is actively in the conversation yet. To reactivate a DM or group conversation: - A participant, or a preexisting installation belonging to the user who ran the import, will automatically add the new installation when sending a message to the conversation or calling `conversation.sync`. In some cases, it may take up to 30 minutes for other devices to recognize the new installation. - For DM conversations, you may choose to programmatically create a duplicate DM for every inactive DM to trigger [stitching](/chat-apps/push-notifs/understand-push-notifs#dm-stitching-considerations-for-push-notifications). This will activate the DM conversations. Inactive conversations in which participants frequently send messages may seem to activate immediately. ## Archive-based backups vs History Sync Archive-based backups and History Sync serve similar purposes: helping you restore messages on new devices. When should you use one or the other? - If your users sign in using wallets or passkeys shared by multiple chat apps provided by different developers, use [History Sync](/chat-apps/list-stream-sync/history-sync) to allow them to synchronize their messages when they install a new app. - If your goal is to synchronize a user's message history across multiple devices that will be used at the same time, [History Sync](/chat-apps/list-stream-sync/history-sync) may be a better fit. Especially if those devices are on different platforms (an Android tablet and an iOS app). - If your goal is to maintain message history when a user upgrades or replaces their device, use archive-based backups. - If your goal is to allow users to export their data into the storage platform of their choosing, use archive-based backups. ## FAQ about archive-based backups ### What backup frequency options should I offer users? Consider providing backup frequency options based on patterns in common chat apps: - **Never** - For users who prefer manual control - **Daily** - For active users (automatically at a quiet time like 2 AM) - **Weekly** - For moderate users (for example, every Sunday) - **Monthly** - For light users (for example, first day of each month) You might also offer **manual backup** that enables users to create backups on demand ### How often should users create backups? The frequency of archive creation depends on your users' messaging patterns and risk tolerance: For most users: - Weekly or monthly automated backups work well for regular message activity - Consider creating archives after important conversations or when users receive many messages For power users: - Daily automated backups for users who send/receive many messages - Allow manual backup creation before important events (travel, device changes) Backup triggers to consider: - Time-based: Daily, weekly, or monthly schedules - Activity-based: After a certain number of new messages (for example, every 100 messages) - Event-based: Before OS updates, app updates, or when users plan to switch devices ### Should users keep multiple backups? Consider enabling users to maintain multiple backups: - Keep the last 3-5 archives to provide recovery options if one archive becomes corrupted - Implement automatic cleanup of old archives to manage storage space - Allow users to name archives (for example, "Before iOS update" or "Monthly backup - Jan 2024") for easier identification ### How should I handle archive storage locations? Follow patterns established by common chat apps: - iCloud - Google Drive - User's preferred cloud service - Your servers ### Should I automatically prompt users to create archives? Consider gentle prompts similar to common chat apps: - After app installation: Suggest setting up automated backups during onboarding - Periodic reminders: Monthly prompts for users who haven't created recent archives (avoid being pushy) - Before major events: Prompt before app updates or when users haven't backed up in a while - Smart detection: Suggest backups when users have many new active conversations ### How do I help users understand archive limitations? Be transparent about the post-import experience: - Explain read-only mode: Let users know that imported conversations start as read-only - Set expectations: Explain the reactivation process and that reactivation happens automatically but may take time - Provide progress indicators: Show users when conversations become active again ## chat-apps/list-stream-sync/history-sync.mdx # Enable history sync for apps built with XMTP :::warning[This feature is in beta] History sync is still in active development as we work on its reliability and performance. To provide feedback on this feature, please post to [Ideas & Improvements](https://community.xmtp.org/c/general/ideas/54) in the XMTP Community Forums. ::: Enable the history sync feature to give your users a way to sync decrypted historical data from an existing app installation to a new app installation. This historical data includes: - Conversations - Conversation messages - Consent state - HMAC keys (for push notifications) History sync enables your users pick up conversations where they left off, regardless of the app installation they use. All they need is a pre-existing and online app installation to provide the data. ## 🎥 walkthrough: History sync This video provides a walkthrough of history sync, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. ## Enable history sync History sync is enabled by default and runs automatically. When [creating a client](/chat-apps/core-messaging/create-a-client), the `historySyncUrl` client option is dynamically set to a default history sync server URL based on your `env` client option setting. - When `env` is set to `dev`, the `historySyncUrl` is set to `https://message-history.dev.ephemera.network/` - When `env` is set to `production`, the `historySyncUrl` is set to `https://message-history.production.ephemera.network` These default servers are managed and operated by [Ephemera](https://ephemerahq.com/), a steward of the development and adoption of XMTP. You can choose to [run your own server](https://github.com/xmtp/xmtp-message-history-server) and set the `historySyncUrl` to your server's URL. If needed, you can turn off history sync by setting the `historySyncUrl` client option to an empty string. ## How history sync works When your app initializes an XMTP client and a `historySyncUrl` client option is present, history sync automatically triggers an initial sync request and creates an encrypted payload.
Diagram showing step 1 of history sync: client initialization triggers sync request and creates encrypted payload
History sync then uploads the payload, sends a sync reply, and pulls all conversation state history into the new app installation, merging it with the existing app installations in the sync group.
Diagram showing step 2 of history sync: payload upload, sync reply, and merging conversation state history across app installations
Ongoing updates to history are streamed automatically. Updates, whether for user consent preferences or messages, are sent across the sync group, ensuring all app installations have up-to-date information. History syncs are accomplished using these components: - Sync group - Sync worker - History server ### Sync group A sync group is a special [Messaging Layer Security](/protocol/security) group that includes all of the user's devices. The sync group is used to send serialized sync messages across the user's devices. The sync group is filtered out of most queries by default so they don't appear in the user's inbox. ### Sync worker A sync worker is a spawned background worker that listens for sync events, and processes them accordingly. This worker: - Emits and updates consent - Creates and consumes archive payloads for and from other devices - Keeps preferences synced across devices ### History server A history server acts as a bucket that holds encrypted sync payloads. The URL location of these payloads and the password (cipher encryption key) to decrypt these payloads is sent over the sync group for the recipient to decrypt. ## FAQ ### A user logged into a new app installation and doesn't see their conversations. What's going on? A debounce feature checks for new app installations, at most, once every 30 minutes. To circumvent the cool-down timer, send a message using a pre-existing app installation. Once [this issue](https://github.com/xmtp/libxmtp/issues/1309) is resolved, conversations will appear almost instantly. ### A user logged into a new app installation and sees their conversations, but no messages. What's going on? Ensure that you've initiated a call to [sync messages](/chat-apps/list-stream-sync/sync-and-syncall#sync-all-new-welcomes-conversations-messages-and-preferences) and that the pre-existing app installation is online to receive the sync request, process and encrypt the payload, upload it to the history server, and send a sync reply message to the new app installation. ### I called a sync method (messages, consent state, or conversations), but nothing is happening. What's going on? After requesting a sync for one app installation, ensure that the pre-existing app installation is online to receive the sync request. ## chat-apps/list-stream-sync/list.mdx # List conversations ## List existing conversations Get a list of existing group chat and DM conversations in the local database. By default, `list` returns only conversations with a [consent state](/chat-apps/user-consent/user-consent#how-user-consent-preferences-are-set) of allowed or unknown. We recommend listing allowed conversations only. This ensures that spammy conversations with a consent state of unknown don't degrade the user experience. To list all conversations regardless of consent state, use the `consentStates` option and pass all three consent states. Conversations are listed in descending order by their `lastMessage` created at value. If a conversation has no messages, the conversation is ordered by its `createdAt` value. :::code-group ```js [Browser] const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed], }); const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed], }); const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed], }); ``` ```js [Node] const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed], }); const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed], }); const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed], }); ``` ```tsx [React Native] // List Conversation items await alix.conversations.list(['allowed']); // List only active conversations (simplified usage) const alixConvoCount1 = await alix.conversations.list({ isActive: true, // Only show active conversations }); // List Conversation items and return only the fields set to true. Optimize data transfer // by requesting only the fields the app needs. await alix.conversations.list({ members: false, consentState: false, description: false, creatorInboxId: false, addedByInboxId: false, isActive: false, lastMessage: true, }); ``` ```kotlin [Kotlin] // List conversations (both groups and dms) val conversations = alix.conversations.list() val filteredConversations = client.conversations.list(consentState = ConsentState.ALLOWED) // List just dms val dms = alix.conversations.listDms() val filteredDms = client.conversations.listDms(consentState = ConsentState.ALLOWED) //List just groups val groups = alix.conversations.listGroups() val filteredGroups = client.conversations.listGroups(consentState = ConsentState.ALLOWED) ``` ```swift [Swift] // List conversations (both groups and dms) let conversations = try await alix.conversations.list() let orderFilteredConversations = try await client.conversations.list(consentState: .allowed) // List just dms let conversations = try await alix.conversations.listDms() let orderFilteredConversations = try await client.conversations.listDms(consentState: .allowed) //List just groups let conversations = try await alix.conversations.listGroups() let orderFilteredConversations = try await client.conversations.listGroups(consentState: .allowed) ``` ::: ## List a user's active conversations The `isActive()` method determines whether the current user is still an active member of a group conversation. For example: - When a user is added to a group, `isActive()` returns `true` for that user - When a user is removed from a group, `isActive()` returns `false` for that user :::code-group ```js [Browser] // Check if user is active in a specific conversation const isUserActive = await conversation.isActive(); console.log('User is active:', isUserActive); // Filter active conversations const activeConversations = []; for (const conversation of allConversations) { if (await conversation.isActive()) { activeConversations.push(conversation); } } ``` ```js [Node] // Check if user is active in a specific conversation const isUserActive = conversation.isActive; console.log('User is active:', isUserActive); // Filter active conversations (direct property access) const activeConversations = allConversations.filter( (conversation) => conversation.isActive ); ``` ```tsx [React Native] // Check if user is active in a specific conversation const isUserActive = await conversation.isActive(); console.log('User is active:', isUserActive); // Filter active conversations const activeConversations = []; for (const conversation of allConversations) { if (await conversation.isActive()) { activeConversations.push(conversation); } } // Or filter conversations by active status when listing (simplified) const conversations = await alix.conversations.list({ isActive: true, // Only show active conversations }); ``` ::: You can use a user's active status to filter conversations and potentially have a separate section for "archived" or "inactive" conversations. ## chat-apps/list-stream-sync/stream.mdx # Stream conversations and messages ## Stream new group chat and DM conversations Listens to the network for new group chats and DMs. Whenever a new conversation starts, it triggers the provided callback function with a [`ConversationContainer` object](/chat-apps/core-messaging/create-conversations#conversation-union-type). This allows the client to immediately respond to any new group chats or DMs initiated by other users. :::code-group ```js [Browser] const stream = await client.conversations.stream({ onValue: (conversation) => { // Received a conversation console.log('New conversation:', conversation); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Stream failed'); }, }); // Or use for-await loop for await (const conversation of stream) { // Received a conversation console.log('New conversation:', conversation); } ``` ```js [Node] const stream = await client.conversations.stream({ onValue: (conversation) => { // Received a conversation console.log('New conversation:', conversation); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Stream failed'); }, }); // To stream only groups const groupStream = await client.conversations.streamGroups({ onValue: (conversation) => { console.log('New group:', conversation); }, }); // To stream only DMs const dmStream = await client.conversations.streamDms({ onValue: (conversation) => { console.log('New DM:', conversation); }, }); // Or use for-await loop for await (const conversation of stream) { // Received a conversation console.log('New conversation:', conversation); } ``` ```tsx [React Native] await alix.conversations.stream(async (conversation: Conversation) => { // Received a conversation }); ``` ```kotlin [Kotlin] alix.conversations.stream(type: /* OPTIONAL DMS, GROUPS, ALL */).collect { // Received a conversation } ``` ```swift [Swift] for await convo in try await alix.conversations.stream(type: /* OPTIONAL .dms, .groups, .all */) { // Received a conversation } ``` ::: ## Stream new group chat and DM messages This function listens to the network for new messages within all active group chats and DMs. Whenever a new message is sent to any of these conversations, the callback is triggered with a `DecodedMessage` object. This keeps the inbox up to date by streaming in messages as they arrive. By default, `streamAll` streams only conversations with a [consent state](/chat-apps/user-consent/user-consent#how-user-consent-preferences-are-set) of allowed or unknown. We recommend streaming messages for allowed conversations only. This ensures that spammy conversations with a consent state of unknown don't take up networking resources. This also ensures that unwanted spam messages aren't stored in the user's local database. To stream all conversations regardless of consent state, pass `[Allowed, Unknown, Denied]`. :::warning[Important] The stream is infinite. Therefore, any looping construct used with the stream won't terminate unless you explicitly initiate the termination. You can initiate the termination by breaking the loop or by making an external call to `return`. ::: :::code-group ```js [Browser] // stream all messages from conversations with a consent state of allowed const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { // Received a message console.log('New message:', message); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Stream failed'); }, }); // Or use for-await loop for await (const message of stream) { // Received a message console.log('New message:', message); } ``` ```js [Node] // stream all messages from conversations with a consent state of allowed const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { // Received a message console.log('New message:', message); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Stream failed'); }, }); // stream only group messages const groupMessageStream = await client.conversations.streamAllGroupMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { console.log('New group message:', message); }, }); // stream only dm messages const dmMessageStream = await client.conversations.streamAllDmMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { console.log('New DM message:', message); }, }); // Or use for-await loop for await (const message of stream) { // Received a message console.log('New message:', message); } ``` ```tsx [React Native] await alix.conversations.streamAllMessages( async (message: DecodedMessage) => { // Received a message }, { consentState: ['allowed'] } ); ``` ```kotlin [Kotlin] alix.conversations.streamAllMessages(type: /* OPTIONAL DMS, GROUPS, ALL */, consentState: listOf(ConsentState.ALLOWED)).collect { // Received a message } ``` ```swift [Swift] for await message in try await alix.conversations.streamAllMessages(type: /* OPTIONAL .dms, .groups, .all */, consentState: [.allowed]) { // Received a message } ``` ::: ## Handle stream failures :::warning **Browser and Node SDK** Streams will automatically attempt to reconnect if they fail. By default, a stream will attempt to reconnect up to 10 times with a 60 second delay between each retry. To change these defaults, use the `retryAttempts` and `retryDelay` options. To disable this feature, set the `retryOnFail` option to `false`. During the retry process, the `onRetry` and `onRestart` callbacks can be used to monitor progress. ::: :::code-group ```ts [Node] // disable automatic reconnects const stream = await client.conversations.streamAllMessages({ retryOnFail: false, onValue: (message) => { console.log('New message:', message); }, }); // use stream options with retry configuration const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], retryAttempts: 10, retryDelay: 20000, // 20 seconds onValue: (message) => { console.log('New message:', message); }, onError: (error) => { console.error('Stream error:', error); }, onFail: () => { console.log('Stream failed after retries'); }, onRestart: () => { console.log('Stream restarted'); }, onRetry: (attempt, maxAttempts) => { console.log(`Stream retry attempt ${attempt} of ${maxAttempts}`); }, }); ``` ```tsx [Browser] // Browser SDK also supports stream retry options const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], retryAttempts: 5, retryDelay: 15000, // 15 seconds onValue: (message) => { console.log('New message:', message); }, onError: (error) => { console.error('Stream error:', error); }, onFail: () => { console.log('Stream failed after retries'); }, onRestart: () => { console.log('Stream restarted'); }, onRetry: (attempt, maxAttempts) => { console.log(`Stream retry attempt ${attempt} of ${maxAttempts}`); }, }); ``` ```tsx [React Native] const [messages, setMessages] = useState([]); const messageCallback = async (message: DecodedMessage) => { setMessages((prev) => [...prev, message]); }; const conversationFilterType: ConversationFilterType = 'all'; const consentStates: ConsentState[] = ['allowed']; const onCloseCallback = () => { console.log('Message stream closed, handle retries here'); }; const startMessageStream = async () => { await alix.conversations.streamAllMessages( messageCallback, conversationFilterType, consentStates, onCloseCallback ); }; ``` ```kotlin [Kotlin] private val _messages = MutableStateFlow>(emptyList()) val messages: StateFlow> = _messages.asStateFlow() fun startMessageStream() { viewModelScope.launch { streamMessages(onClose = { Log.d("XMTP ViewModel", "Message stream closed.") }).collect { decodedMessage -> _messages.update { current -> current + decodedMessage } } } } ``` ```swift [Swift] @Published private(set) var messages: [DecodedMessage] = [] private var streamTask: Task? = nil func startMessageStream(from conversation: XMTPConversation) { streamTask?.cancel() streamTask = Task { do { for try await message in conversation.streamMessages(onClose: { print("XMTP ViewModel: Message stream closed.") }) { messages.append(message) } } catch { print("XMTP ViewModel: Stream failed with error \(error)") } } } func stopMessageStream() { streamTask?.cancel() streamTask = nil } ``` ::: ## chat-apps/list-stream-sync/sync-and-syncall.mdx # Sync conversations and messages :::tip[Note] Syncing does not refetch existing conversations and messages. It also does not fetch messages for group chats you are no longer a part of. ::: ## 🎥 walkthrough: Syncing This video provides a walkthrough of key concepts required to implement syncing correctly. ## Sync a specific conversation Get all new messages and group updates (name, description, etc.) for a specific conversation from the network. :::code-group ```js [Browser] await client.conversation.sync(); ``` ```js [Node] await client.conversation.sync(); ``` ```tsx [React Native] await client.conversation.sync(); ``` ```kotlin [Kotlin] client.conversation.sync() ``` ```swift [Swift] try await client.conversation.sync() ``` ::: ## Sync new conversations Get any new group chat or DM conversations from the network. :::code-group ```js [Browser] await client.conversations.sync(); ``` ```js [Node] await client.conversations.sync(); ``` ```tsx [React Native] await client.conversations.sync(); ``` ```kotlin [Kotlin] client.conversations.sync() ``` ```swift [Swift] try await client.conversations.sync() ``` ::: ## Sync all new welcomes, conversations, messages, and preferences Sync all new welcomes, group chat and DM conversations, messages, and [preference updates](/chat-apps/list-stream-sync/sync-preferences) from the network. By default, `syncAll` streams only conversations with a [consent state](/chat-apps/user-consent/user-consent#how-user-consent-preferences-are-set) of allowed or unknown. We recommend streaming messages for allowed conversations only. This ensures that spammy conversations with a consent state of unknown don't take up networking resources. This also ensures that unwanted spam messages aren't stored in the user's local database. To sync all conversations regardless of consent state, pass `[ALLOWED, UNKNOWN, DENIED]`. To sync preferences only, you can call [`preferences.sync`](/chat-apps/list-stream-sync/sync-preferences). Note that `preferences.sync` will also sync welcomes to ensure that you have all potential new installations before syncing. :::code-group ```js [Browser] await client.conversations.syncAll(['allowed']); ``` ```js [Node] await client.conversations.syncAll(['allowed']); ``` ```tsx [React Native] await client.conversations.syncAllConversations(['allowed']); ``` ```kotlin [Kotlin] client.conversations.syncAllConversations(consentState = listOf(ConsentState.ALLOWED)) ``` ```swift [Swift] try await client.conversations.syncAllConversations(consentState: [.allowed]) ``` ::: ## chat-apps/list-stream-sync/sync-preferences.mdx # Sync preferences You can sync the following preference-related information across multiple existing app installations: - Conversation [consent preferences](/chat-apps/user-consent/user-consent#how-user-consent-preferences-are-set) - Conversation HMAC keys (for [push notifications](/chat-apps/push-notifs/understand-push-notifs)) ## Databases used by preferences sync Each XMTP node contains: - A **welcomes database**: Keeps a ledger of all groups an inbox ID is a part of. - A **group messages database**: Keeps a ledger of all messages in the groups an inbox ID is a part of.
databases in each xmtpd node
In the welcomes database, the groups are of these types: - **1:1 chat**: A group that handles conversations between an inbox ID and one other user. - **Group chat**: A group that handles conversations between an inbox ID and multiple other users. - **Preferences**: A group that handles syncing preferences between an inbox ID's existing app installations. One-to-one chat, group chat, and preferences groups in the welcome database are updated by apps as follows: - 1:1 chats - `conversations.list` - `syncAll` - `streamAll` - Group chats - `conversations.list` - `syncAll` - `streamAll` - Preferences - `preferences.sync` - `syncAll`
groups in the welcome database
## Preferences group To describe preference sync, let's first focus on how the preferences group works. 1. A preferences group has one member, which is one inbox ID. 2. Let's say Inbox ID Alix has an Installation A of App A on their phone. At this time, Inbox ID Alix has a preferences group that looks like this:
Diagram showing preferences group with one member (Inbox ID Alix) and Installation A of App A
3. Inbox ID Alix then logs in to an Installation B of App B on their phone. The next time Installation A runs `preferences.sync` or `syncAll`, it updates the preferences group as follows:
Diagram showing preferences group updated to include Installation B of App B after Alix logs in to a second app
4. Then let's say Inbox ID Alix logs in to an Installation C of App A on their tablet. The next time Installation A or B runs `preferences.sync` or `syncAll`, it updates the preferences group as follows:
Diagram showing preferences group updated to include Installation C of App A on tablet after Alix logs in to a third installation
## Preferences sync worker Now, let's describe how the preferences sync worker helps keep user consent preferences in sync across existing app installations. 1. Let's say Inbox ID Alix uses Installation A to block Inbox ID Bo.
Diagram showing Installation A blocking Inbox ID Bo
2. This sends a message to the preferences group in the group message database. This is not an actual chat message, but a serialized proto message that is not shown to app users.
Diagram showing serialized proto message sent to preferences group in the group message database
3. When Installation B calls `preferences.sync` or `syncAll`, it gets the message from the preferences group. The sync worker listens for these preferences group messages and processes the message to block Inbox ID Bo in Installation B.
Diagram showing Installation B processing the sync message to block Inbox ID Bo
4. Likewise, when Installation C calls `preferences.sync` or `syncAll`, it gets the message from the preferences group, and the sync worker ensures Inbox ID Bo is blocked there as well.
Diagram showing Installation C processing the sync message to block Inbox ID Bo
Preferences sync handles HMAC keys in the same way. For example, when a new installation generates a root HMAC key and publishes it to the sync group, other installations will eventually replace their root HMAC keys with the last one posted. When Installation A and C call `preferences.sync` or `syncAll`, the sync worker gets the latest root HMAC key message from the preferences group and ensures that all installations converge to use the same root HMAC key. This root key is then used to derive group-specific HMAC keys for each conversation. ## Sync preferences Use this call to sync preferences (consent preferences and HMAC keys) across app installations in a preferences group. This will also sync welcomes to ensure that you have all potential new installations before syncing. This is a lighter-weight alternative to syncing preferences using [`syncAll`](/chat-apps/list-stream-sync/sync-and-syncall). :::code-group ```js [Browser] await client.preferences.sync(); ``` ```js [Node] await client.preferences.sync(); ``` ```tsx [React Native] await client.preferences.sync(); ``` ```kotlin [Kotlin] client.preferences.sync() ``` ```swift [Swift] try await client.preferences.sync() ``` ::: ## chat-apps/intro/build-with-llms.mdx # Use XMTP documentation with AI coding assistants To make it easier for you to build with XMTP using LLMs and coding assistants, we offer the following AI-ready documentation files. If you're using an AI coding assistant that allows custom context, you can upload or point to the appropriate file to enhance your development experience with XMTP. Using the file focused on your builder use case (chat apps or agents) typically provides better AI performance. ## For building chat apps Use `llms-chat-apps.txt`: [https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-chat-apps.txt](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-chat-apps.txt) Includes: Core messaging, content types, push notifications, user consent, protocol fundamentals, network operations, and funding information. ## For building agents Use `llms-agents.txt`: [https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-agents.txt](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-agents.txt) Includes: Agent concepts, building and deploying agents, agent-specific content types, protocol fundamentals, network operations, and funding information. ## For comprehensive coverage Use `llms-full.txt`: [https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-full.txt](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/llms/llms-full.txt) Includes: All documentation for chat apps, agents, protocol fundamentals, network operations, and funding information. ## chat-apps/intro/dev-support.mdx # Get dev support for building with XMTP Building with XMTP and need help or think you've found a bug? Please open an issue in the relevant repo: - [Browser SDK](https://github.com/xmtp/xmtp-js/issues) - [Node SDK](https://github.com/xmtp/xmtp-js/issues) - [React Native SDK](https://github.com/xmtp/xmtp-react-native/issues) - [Android SDK](https://github.com/xmtp/xmtp-android/issues) - [iOS SDK](https://github.com/xmtp/xmtp-ios/issues) For all other topics, post to the [XMTP Community Forums](https://community.xmtp.org/). ## chat-apps/intro/faq.mdx # FAQ about XMTP Get answers to the most frequently asked questions about XMTP. ## What works with XMTP? In the spirit of web3 composability, here are **just a few** of the building blocks that work well with XMTP. Building your app with these tools can help you deliver and distribute an app—faster and with quality. :::tip This list is not exhaustive and is just a starting point. A highly extensible protocol like XMTP can work with more tools than those listed in each section. ::: ### Wallet connectors Here are some options for connecting wallets to your app built with XMTP: - [RainbowKit](https://www.rainbowkit.com/) Support for WalletConnect v2 is now standard in RainbowKit. To learn how to upgrade, see [Migrating to WalletConnect v2](https://www.rainbowkit.com/guides/walletconnect-v2). - [Thirdweb](https://thirdweb.com/) - [wagmi](https://wagmi.sh/) ### Message payload storage Here are some options for storing encrypted message payload content: - [IPFS](https://ipfs.io/) - [ThirdwebStorage](https://portal.thirdweb.com/infrastructure/storage/how-storage-works) - [web3.storage](https://web3.storage/) ### Wallet apps XMTP can be used with EVM-compatible wallet apps that support ECDSA signing on the secp256k1 elliptic curve. These include common wallet apps such as: - [Coinbase Wallet](https://www.coinbase.com/wallet) - [MetaMask](https://metamask.io/) - [Rainbow Wallet](https://rainbow.me/) - Most wallets in the [WalletConnect network](https://explorer.walletconnect.com/?type=wallet) The XMTP SDK **does not** include a wallet app abstraction, as XMTP assumes that developers have a way to obtain a wallet app connection. ### Chains XMTP can work with signatures from any private public key pair and currently supports EOAs and SCWs on Ethereum and Ethereum side-chains and L2s. Because all Ethereum Virtual Machine (EVM) chains share the same Ethereum wallet and address format and XMTP messages are stored off-chain, XMTP is interoperable across EVM chains, including testnets. (XMTP itself does not use EVMs.) For example, whether a user has their wallet app connected to Ethereum or an Ethereum side-chain or L2, their private key can generate and retrieve their XMTP key pair to give them access to XMTP. XMTP is also chain-agnostic, so multi-chain support is possible. Here are just a few chains that work with XMTP: - [Arbitrum](https://arbitrum.foundation/) - [Avalanche](https://www.avax.network/) - [Base](https://base.org/) - [(BNB) Chain](https://www.bnbchain.org/) - [Ethereum](https://ethereum.org/) - [zk-EVM](https://linea.build/) - [Optimism](https://www.optimism.io/) - [Polygon](https://polygon.technology/) - [Scroll](https://www.scroll.io/) ## Build with XMTP ### Which languages does the XMTP SDK support? XMTP SDKs are [available for multiple languages](https://docs.xmtp.org/#start-building). ### Which web3 libraries does the XMTP SDK require? The XMTP SDK currently requires you to use [ethers](https://ethers.org/) or another web3 library capable of supplying an [ethers Signer](https://docs.ethers.io/v5/api/signer/), such as [wagmi](https://wagmi.sh/). ### What is the invalid key package error? ### Where can I get official XMTP brand assets? See the [XMTP brand guidelines](https://github.com/xmtp/brand) GitHub repo. ## Network See [Network FAQ](/network/run-a-node/#faq-about-the-xmtp-network). ## Fees ### Does XMTP have fees? XMTP core developers and researchers are working on a specific fee model for XMTP, with the following guiding principles in mind: - Fees will be dynamic to protect the network from Denial of Service (DoS) attacks. - Infrastructure costs for the network must remain low even when decentralized, and comparable to the costs for an equivalent centralized messaging service. - There must be a low "take rate": the biggest driver of cost must be infrastructure costs, with any remaining cost returned to the network. Have questions or feedback about the fee model for XMTP? See [XIP-57: Messaging fee collection](https://community.xmtp.org/t/xip-57-messaging-fee-collection/876) in the XMTP Community Forums. ## Security ### Has XMTP undergone a security audit? A security assessment of [LibXMTP](https://github.com/xmtp/libxmtp) and its use of Messaging Layer Security (MLS) was completed by [NCC Group](https://www.nccgroup.com/) in Dec 2024. See [Public Report: XMTP MLS Implementation Review](https://www.nccgroup.com/us/research-blog/public-report-xmtp-mls-implementation-review/). ## Storage ### Where are XMTP messages stored? XMTP stores messages in the XMTP Network before and after retrieval. App-specific message storage policies may vary. ### What are the XMTP message retention policies? #### For xmtpd and the decentralized XMTP Network xmtpd (XMTP daemon) is the XMTP node software that powers the decentralized XMTP Testnet, as well as the forthcoming decentralized XMTP Mainnet. To learn about xmtpd's message retention policies, see [XIP-69: Message retention and expiry strategy for xmtpd](https://community.xmtp.org/t/xip-69-message-retention-and-expiry-strategy-for-xmtpd/955) #### For the on-device database managed by the XMTP SDK Messages are stored for as long as the user decides to keep them. However, encryption keys are regularly rotated. #### For the current production XMTP Network Encrypted payloads are stored indefinitely. ### What are XMTP message storage and retrieval costs? Messages are stored off-chain on the XMTP network, with all nodes currently hosted by Ephemera. Ephemera currently absorbs all message storage and retrieval costs. Today, there are no message storage and retrieval-related fees incurred by developers for building with the XMTP SDK. ## Messages ### Which message formats does XMTP support? XMTP transports a message payload as a set of bytes that can represent any format that a developer wants to support, such as plain text, JSON, or non-text binary or media content. With XMTP, these message formats are called content types. Currently, there are two basic content types available. These content types aim to establish broad compatibility among apps built with XMTP. The XMTP community can propose and adopt standards for other content types, either informally or through a governance process. To learn more about content types, see [Content types](/chat-apps/content-types/content-types). To learn more about the XMTP improvement proposals governance process, see [What is an XIP?](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-0-purpose-process.md) ### Does XMTP have a maximum message size? Yes. Messages sent on the XMTP network are limited to just short of 1MB (1048214 bytes). For this reason, XMTP supports [remote attachments](/chat-apps/content-types/attachments). ### Does XMTP support message deletion and editing? Not currently. However, Ephemera is exploring ways to support message deletion and editing. Have ideas about message deletion and editing? Post to the [XMTP Community Forums](https://community.xmtp.org/c/development/ideas/54). ### Is XMTP more like email or chat? XMTP enables developers to implement messaging features and UX paradigms that best fit their needs. As a result, messages sent using apps built with XMTP might resemble many typical forms of communication, such as email, direct and group messaging, broadcasts, text messaging, push notifications, and more. ## What is Ephemera? Ephemera is a web3 software company that contributes to XMTP, an open network, protocol, and standards for secure messaging between blockchain accounts. Ephemera employees work alongside other XMTP community members to build with and extend XMTP. Community [contributions and participation](https://community.xmtp.org/) are critical to the development and adoption of XMTP. Ephemera focuses on serving developers. We build [SDKs](https://docs.xmtp.org/#start-building), developer tools, and example apps that help developers build great experiences with XMTP. Ephemera [acquired Converse](https://paragraph.xyz/@ephemera/converse) in June 2024. Converse is now [Convos](https://github.com/xmtplabs/convos-app), open-sourced for the entire XMTP network. ## chat-apps/intro/get-started.mdx import { SdkButtons, SdkButton } from '../../../components/SdkButtons'; # Get started building with XMTP XMTP (Extensible Message Transport Protocol) is the largest and most secure decentralized messaging network. XMTP is open and permissionless, empowering any developer to build end-to-end encrypted 1:1, group, and agent messaging experiences, and more. To learn more, see [Why build with XMTP?](/chat-apps/intro/why-xmtp). ## 🛠️ Phase 0: Explore XMTP developer tools - Pick your SDK: Browser Node React Native Android iOS - [Use llms-full.txt](/chat-apps/intro/build-with-llms) to provide the full text of the XMTP developer documentation to an AI coding assistant. - [Use xmtp.chat](https://xmtp.chat), the official web chat app for developers, to interact with and test your app - [Run a local XMTP node](https://github.com/xmtp/xmtp-local-node/tree/main) for development and testing. ## 💬 Phase I: Build core messaging 1. [Create an EOA or SCW signer](/chat-apps/core-messaging/create-a-signer). 2. [Create an XMTP client](/chat-apps/core-messaging/create-a-client). Be sure to set the `appVersion` client option. 3. [Check if an identity is reachable on XMTP](/chat-apps/core-messaging/create-conversations#check-if-an-identity-is-reachable). 4. Create a [group chat](/chat-apps/core-messaging/create-conversations) or [direct message](/chat-apps/core-messaging/create-conversations) (DM) conversation. With XMTP, "conversation" refers to both group chat and DM conversations. 5. [Send messages](/chat-apps/core-messaging/send-messages) in a conversation. 6. Manage group chat [permissions](/chat-apps/core-messaging/group-permissions) and [metadata](/chat-apps/core-messaging/group-metadata). 7. [Manage identities, inboxes, and installations](/chat-apps/core-messaging/manage-inboxes). 8. Be sure to [observe rate limits](/chat-apps/core-messaging/rate-limits). ## 📩 Phase II: Manage conversations and messages 1. [List existing conversations](/chat-apps/list-stream-sync/list) from local storage. 2. [Stream new conversations](/chat-apps/list-stream-sync/stream) from the network. 3. [Stream new messages](/chat-apps/list-stream-sync/stream) from the network. 4. [Sync new conversations](/chat-apps/list-stream-sync/sync-and-syncall) from the network. 5. [Sync a specific conversation's messages and preference updates](/chat-apps/list-stream-sync/sync-and-syncall) from the network. ## 💅🏽 Phase III: Enhance the user experience 1. [Implement user consent](/chat-apps/user-consent/support-user-consent), which provides a consent value of either **unknown**, **allowed** or **denied** to each of a user's contacts. You can use these consent values to filter conversations. For example: - Conversations with **allowed** contacts go to a user's main inbox - Conversations with **unknown** contacts go to a possible spam tab - Conversations with **denied** contacts are hidden from view. 2. Support rich [content types](/chat-apps/content-types/content-types). - [Attachments](/chat-apps/content-types/attachments) - Single remote attachment - Multiple remote attachments - Attachments smaller than 1MB - [Reactions](/chat-apps/content-types/reactions) - [Replies](/chat-apps/content-types/replies) - [Read receipts](/chat-apps/content-types/read-receipts) - [Onchain transactions](/chat-apps/content-types/transactions) - [Onchain transaction references](/chat-apps/content-types/transaction-refs) 3. [Implement push notifications](/chat-apps/push-notifs/understand-push-notifs), if applicable. ## 🧪 Phase IV: Test and debug - [Stress and burn-in test](/chat-apps/debug-your-app#xmtp-debug) your chat app. - [Enable file logging](/chat-apps/debug-your-app#file-logging). - [Capture network statistics](/chat-apps/debug-your-app#network-statistics). - Found a bug or need help? Contact [dev support](/chat-apps/intro/dev-support). ## chat-apps/intro/why-xmtp.mdx # Why build with XMTP? Build with XMTP to: - **Deliver secure and private messaging** Using the [Messaging Layer Security](/protocol/security) (MLS) standard, a ratified [IETF](https://www.ietf.org/about/introduction/) standard, XMTP provides end-to-end encrypted messaging with forward secrecy and post-compromise security. - **Provide spam-free chats** In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP [user consent preferences](/chat-apps/user-consent/user-consent), developers can give their users spam-free chats displaying conversations with chosen contacts only. - **Build on native crypto rails** Build with XMTP to tap into the capabilities of crypto and web3. Support decentralized identities, crypto transactions, and more, directly in a messaging experience. - **Empower users to own and control their communications** With apps built with XMTP, users own their conversations, data, and identity. Combined with the interoperability that comes with protocols, this means users can access their end-to-end encrypted communications using any app built with XMTP. - **Create with confidence** Developers are free to create the messaging experiences their users want—on a censorship-resistant protocol architected to last forever. Because XMTP isn't a closed proprietary platform, developers can build confidently, knowing their access and functionality can't be revoked by a central authority. ## Try an app built with XMTP One of the best ways to understand XMTP is to use an app built with XMTP. Try [xmtp.chat](https://xmtp.chat/), an app made for devs to learn to build with XMTP—using an app built with XMTP. ## Join the XMTP community - **XMTP builds in the open** - Explore the documentation on this site - Explore the open [XMTP GitHub org](https://github.com/xmtp), which contains code for LibXMTP, XMTP SDKs, and xmtpd, node software powering the XMTP testnet. - Explore the open source code for [xmtp.chat](https://github.com/xmtp/xmtp-js/tree/main/apps/xmtp.chat), an app made for devs to learn to build with XMTP—using an app built with XMTP. - **XMTP is for everyone** - [Join the conversation](https://community.xmtp.org/) and become part of the movement to redefine digital communications. ## chat-apps/user-consent/support-user-consent.mdx # Support user consent preferences to provide spam-free chats Use the following methods to provide users with control over their messaging experience, ensuring their chats are tailored to their preferences and spam-free. ## Sync new consent preferences from the network You can sync new consent preferences (and HMAC keys) from the network using any of these calls: - [Sync preferences only](/chat-apps/list-stream-sync/sync-preferences) - [Sync all new conversations, messages, and preferences](/chat-apps/list-stream-sync/sync-and-syncall) - [Stream all group chat and DM messages and preferences](/chat-apps/list-stream-sync/stream) ## Get the consent state of a conversation Check the current consent state of a specific conversation: :::code-group ```js [Browser] import { ConsentEntityType } from '@xmtp/browser-sdk'; // get consent state from the client const conversationConsentState = await client.getConsentState( ConsentEntityType.GroupId, groupId ); // or get consent state directly from a conversation const groupConversation = await client.conversations.findConversationById(groupId); const groupConversationConsentState = await groupConversation.consentState(); ``` ```js [Node] import { ConsentEntityType } from '@xmtp/node-sdk'; // get consent state from the client const conversationConsentState = await client.getConsentState( ConsentEntityType.GroupId, groupId ); // or get consent state directly from a conversation const groupConversation = await client.conversations.findConversationById(groupId); const groupConversationConsentState = await groupConversation.consentState(); ``` ```tsx [React Native] await conversation.consentState(); ``` ```kotlin [Kotlin] conversation.consentState() ``` ```swift [Swift] try conversation.consentState() ``` ::: ## Update the conversation consent state Update the consent state of a conversation to allow or deny messages: :::code-group ```js [Browser] import { ConsentEntityType, ConsentState } from '@xmtp/browser-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ { entityId: groupId, entityType: ConsentEntityType.GroupId, state: ConsentState.Allowed, }, ]); // set consent state directly on a conversation const groupConversation = await client.conversations.findConversationById(groupId); await groupConversation.updateConsentState(ConsentState.Allowed); ``` ```js [Node] import { ConsentEntityType, ConsentState } from '@xmtp/node-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ { entityId: groupId, entityType: ConsentEntityType.GroupId, state: ConsentState.Allowed, }, ]); // set consent state directly on a conversation const groupConversation = await client.conversations.findConversationById(groupId); await groupConversation.updateConsentState(ConsentState.Allowed); ``` ```tsx [React Native] await conversation.updateConsent('allowed'); // 'allowed' | 'denied' ``` ```kotlin [Kotlin] conversation.updateConsent(ALLOWED) // ALLOWED | DENIED ``` ```swift [Swift] try await conversation.updateConsent(.allowed) // .allowed | .denied ``` ::: ## Stream consent preferences in real-time Listen for real-time updates to consent preferences: :::code-group ```tsx [Browser] // Stream consent records in real-time const stream = await client.preferences.streamConsent({ onValue: (updates) => { // Received consent updates console.log('Consent updates:', updates); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Consent stream failed'); }, }); // Or use for-await loop for await (const updates of stream) { // Received consent updates console.log('Consent updates:', updates); } ``` ```tsx [Node] // Stream consent records in real-time const stream = await client.preferences.streamConsent({ onValue: (updates) => { // Received consent updates console.log('Consent updates:', updates); }, onError: (error) => { // Log any stream errors console.error(error); }, onFail: () => { console.log('Consent stream failed'); }, }); // Or use for-await loop for await (const updates of stream) { // Received consent updates console.log('Consent updates:', updates); } ``` ```tsx [React Native] await client.preferences.streamConsent(); ``` ```kotlin [Kotlin] client.preferences.streamConsent().collect { // Received ConsentRecord } ``` ```swift [Swift] for await consent in try await client.preferences.streamConsent() { // Received consent } ``` ::: ## Update consent for an individual in a group chat Update the consent state for an individual in a group chat: :::tip[Note] You may want to enable users to deny or allow a users on an individual basis. You can then update the group chat UI to hide messages from denied individuals. ::: :::code-group ```js [Browser] import { ConsentEntityType, ConsentState } from '@xmtp/browser-sdk'; await client.setConsentStates([ { entityId: inboxId, entityType: ConsentEntityType.InboxId, state: ConsentState.Denied, }, ]); ``` ```js [Node] import { ConsentEntityType, ConsentState } from '@xmtp/node-sdk'; // set consent state from the client (can set multiple states at once) await client.setConsentStates([ { entityId: inboxId, entityType: ConsentEntityType.InboxId, state: ConsentState.Denied, }, ]); ``` ```tsx [React Native] await client.preferences.setConsentState( new ConsentRecord(inboxId, 'inbox_id', 'denied') ); ``` ```kotlin [Kotlin] client.preferences.setConsentState( listOf( ConsentRecord( inboxId, EntryType.INBOX_ID, ConsentState.DENIED ) ) ) ``` ```swift [Swift] try await client.preferences.setConsentState( entries: [ ConsentRecord( value: inboxID, entryType: .inbox_id, consentType: .denied) ]) ``` ::: ## Get the consent state of an individual in a group chat Get the consent state of an individual in a group chat: :::tip[Note] You may want to enable users to deny or allow a users on an individual basis. You can then update the group chat UI to hide messages from denied individuals. ::: :::code-group ```js [Browser] import { ConsentEntityType } from '@xmtp/browser-sdk'; const inboxConsentState = await client.getConsentState( ConsentEntityType.InboxId, inboxId ); ``` ```js [Node] import { ConsentEntityType } from '@xmtp/node-sdk'; const inboxConsentState = await client.getConsentState( ConsentEntityType.InboxId, inboxId ); ``` ```tsx [React Native] // Get consent directly on the member const memberConsentStates = (await group.members()).map((member) => member.consentState() ); // Get consent from the inboxId const inboxConsentState = await client.preferences.inboxIdConsentState(inboxId); ``` ```kotlin [Kotlin] // Get consent directly on the member val memberConsentStates = group.members().map { it.consentState } // Get consent from the inboxId val inboxConsentState = client.preferences.inboxIdState(inboxId) ``` ```swift [Swift] // Get consent directly on the member let memberConsentStates = try await group.members.map(\.consentState) // Get consent from the inboxId let inboxConsentState = try await client.preferences.inboxIdState(inboxId: inboxId) ``` ::: ## See who created and added you to a group Get the inbox ID of the individual who added you to a group or created the group to check the consent state for it: ```tsx [React Native] group.addedByInboxId; await group.creatorInboxId(); ``` ```kotlin [Kotlin] group.addedByInboxId() group.creatorInboxId() ``` ```swift [Swift] try await group.addedByInboxId() try await group.creatorInboxId() ``` ## Handle unknown contacts With user consent preferences, an inbox ID or conversation ID can have one of three user consent preference values in relation to another user's inbox ID: - Unknown - Allowed - Denied You can implement user consent preferences to give your users inboxes that are **spam-free spaces for allowed conversations and contacts only**. You can then handle message requests from unknown contacts in a separate UI. These message requests from unknown contacts could be from: - Contacts the user might know - Contacts the user might not know - Spammy or scammy contacts You can filter these unknown contacts to: - Identify contacts the user might know or want to know and display them on a **You might know** tab, for example. - Identify contacts the user might not know and not want to know, which might include spam, and display them on a **Hidden requests** tab, for example. ### Identify contacts the user might know To identify contacts the user might know or want to know, you can look for signals in onchain data that imply an affinity between addresses. ```kotlin val inboxState = inboxStateForInboxId(inboxId) val identities = inboxState.identities val ethAddresses = identities .filter { it.kind == ETHEREUM } .map { it.identifier } ``` You can then display appropriate messages on a **You might know** tab, for example.
Screenshot of a mobile chat app showing a 'You might know' tab with filtered conversation requests
### Identify contacts the user might not know, including spammy or scammy requests To identify contacts the user might not know or not want to know, which might include spam, you can consciously decide to scan messages in an unencrypted state to find messages that might contain spammy or scammy content. You can also look for an absence of onchain interaction data between the addresses, which might indicate that there is no affinity between addresses. You can then filter the appropriate messages to display on a **Hidden requests** tab, for example.
Screenshot of a mobile chat app showing a 'Hidden requests' tab with filtered messages from unknown contacts or potentially spammy content
The decision to scan unencrypted messages is yours as the app developer. If you take this approach: - Handle unencrypted messages with extreme care, and don't store unencrypted messages beyond the time necessary to scan them. - Consider telling users that your app scans unencrypted messages for spammy or scammy content. - Consider making spam and scam message detection optional for users who prefer not to have their messages scanned. ### Why is content moderation handled by apps and not XMTP? XMTP is a decentralized, open protocol built to ensure private, secure, and censorship-resistant communication. As such, XMTP can't read unencrypted messages, and therefore, it also can't scan or filter message contents for spammy or scammy material. The protocol can analyze onchain data signals, such as shared activity between wallet addresses, to infer potential affinities between addresses. However, because all XMTP repositories are open source, malicious actors could inspect these methods and develop workarounds to bypass them. Additionally, applying spam filtering or content moderation directly at the protocol level would introduce centralization, which goes against the decentralized, permissionless, and open ethos of XMTP and web3. A protocol-driven approach could limit interoperability and trust by imposing subjective rules about content across all apps. Instead, content filtering and moderation should be implemented at the app layer. Apps can decide how opinionated or lenient they want to be, tailoring their filtering approach to the needs of their users. For example, one app may choose to aggressively scan and block spam to provide a highly curated experience, attracting users who value more protection. Another app may opt for minimal or no filtering, appealing to users who prioritize full control and unfiltered communication. This flexibility enables different apps to serve different user preferences, fostering an ecosystem where users can choose the experience that best suits them. Whether an app scans messages or not, XMTP ensures that developers remain free to build in line with their own values, without imposing restrictions at the infrastructure level. This separation between the protocol and app layers is crucial to maintaining XMTP's commitment to openness, interoperability, and user choice. :::tip Is your app using a great third-party or public good tool to help with spam and keep chats safe? Open an [issue](https://github.com/xmtp/docs-xmtp-org/issues) to share information about it. ::: ## chat-apps/user-consent/user-consent.mdx # Understand how user consent preferences support spam-free chats In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP, you can give your users chats that are **spam-free spaces for chosen contacts only** by supporting user consent preferences. ## 🎥 walkthrough: Consent This video provides a walkthrough of consent, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. ## How user consent preferences work With user consent preferences, an identity registered on the XMTP network can have one of three user consent preference values in relation to another user's identity: - Unknown - Allowed - Denied For example: 1. `alix.id` starts a conversation with `bo.id`. At this time, `alix.id` is unknown to `bo.id` and the conversation displays in a message requests UI. 2. When `bo.id` views the message request, they express their user consent preference to **Block** or **Accept** `alix.id` as a contact. - If `bo.id` accepts `alix.id` as a contact, their conversation displays in `bo.id`'s main inbox. Because only contacts `bo.id` accepts display in their main inbox, their inbox remains spam-free. - If `bo.id` blocks contact with `alix.id`, remove the conversation from `bo.id`'s view. In an appropriate location in your app, give the user the option to unblock the contact. Your app should aim to handle consent preferences appropriately because they are an expression of user intent. For example, if a user blocked a contact, your app should respect the user's intent to not see messages from the blocked contact. Handling the consent preference incorrectly and showing the user messages from the blocked contact may cause the user to lose trust in your app. These user consent preferences are stored privately in an encrypted consent list on the XMTP network. The consent list is accessible by all apps that a user has authorized. This means a user can accept, or block, a contact once and have that consent respected across all other XMTP apps they use. Be sure to load the latest consent list from the network at appropriate steps in your app flow to ensure that your app can operate using the latest data. ## How user consent preferences are set Here are some of the ways user consent preferences are set: ### Unknown Conversation created in an app on an SDK version **with** user consent support: - For a new conversation that a peer contact wants to start with a user, the consent preference is set to `unknown`. Conversation created in an app on an SDK version **without** user consent support: - For all conversations with any peer contact, the consent preference is set to `unknown`. ### Allowed Conversation created in an app on an SDK version **with** user consent support: - For a new conversation that a user created with a peer contact, the SDK sets the consent preference to `allowed`. The user's creation of the conversation with the contact is considered consent. - For an existing conversation created by a peer contact that hasn't had its consent preference updated on the network (`unknown`) and that the user responds to, the SDK will update the consent preference to `allowed`. The user's response to the conversation is considered consent. - For a peer contact that a user has taken the action to allow, subscribe to, or enable notifications from, for example, the app must update the consent preference to `allowed`. Conversation created in an app on an SDK version **without** user consent support: - There are no scenarios in which a user consent preference will be set to `allowed`. ### Denied Conversation created in an app on an SDK version **with** user consent support: - For a peer contact that a user has taken the action to block, unsubscribe from, or disable notifications from, for example, the app must update the consent preference to `denied`. Conversation created in an app on an SDK version **without** user consent support: - There are no scenarios in which a user consent preference will be set to `denied`. ## chat-apps/sdks/android.mdx # Get started with the XMTP Android SDK Use the [XMTP Android SDK](https://github.com/xmtp/xmtp-android) to build secure, private, and decentralized messaging into your Android app. The guide provides some quickstart code, as well as a map to building a [secure chat app](/protocol/security) with XMTP, including support for: - End-to-end encrypted direct message and group chat conversations - Rich content types (attachments, reactions, replies, and more) - Spam-free chats using user consent preferences ## Quickstart ```kotlin [Kotlin] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. class EOAWallet : SigningKey { override val publicIdentity: PublicIdentity get() = PublicIdentity(IdentityKind.ETHEREUM, key.publicAddress) override val type: SignerType get() = SignerType.EOA override suspend fun sign(message: String): SignedData { return SignedData(key.sign(message = message)) } } // 2. Create the XMTP client val client = Client.create( account = SigningKey, options = ClientOptions( ClientOptions.Api(XMTPEnvironment.PRODUCTION, true), appContext = ApplicationContext(), dbEncryptionKey = keyBytes // 32 bytes ) ) // 3. Start conversations val group = client.conversations.newGroup(inboxIds = listOf(bo.inboxId, caro.inboxId)) val groupWithMeta = client.conversations.newGroup( inboxIds = listOf(bo.inboxId, caro.inboxId), permissionLevel = GroupPermissionPreconfiguration.ALL_MEMBERS, name = "The Group Name", imageUrl = "www.groupImage.com", description = "The description of the group" ) // 4. Send messages val dm = client.conversations.findOrCreateDm(recipientInboxId) dm.send(text = "Hello world") group.send(text = "Hello everyone") // 5. List, stream, and sync // List existing conversations val conversations = client.conversations.list() val filteredConversations = client.conversations.list(consentState = ConsentState.ALLOWED) // Stream all new conversations and new messages from allowed conversations client.conversations.streamAllMessages(consentStates = listOf(ConsentState.ALLOWED)).collect { // Received a message } // Sync all new welcomes, preference updates, conversations, // and messages from allowed conversations client.conversations.syncAll(consentStates = ConsentState.ALLOWED) ``` ## chat-apps/sdks/browser.mdx # Get started with the XMTP Browser SDK Use the [XMTP Browser SDK](https://github.com/xmtp/xmtp-js/tree/main/sdks/browser-sdk) to build web-based apps, tools, and experiences with secure, private, and decentralized messaging. The guide provides some quickstart code, as well as a map to building a [secure chat app](/protocol/security) with XMTP, including support for: - End-to-end encrypted direct message and group chat conversations - Rich content types (attachments, reactions, replies, and more) - Spam-free chats using user consent preferences ## Installation Install `@xmtp/browser-sdk` as a dependency in your project. :::code-group ```bash [npm] npm i @xmtp/browser-sdk ``` ```bash [pnpm] pnpm i @xmtp/browser-sdk ``` ```bash [yarn] yarn add @xmtp/browser-sdk ``` ```bash [bun] bun i @xmtp/browser-sdk ``` ::: ## Quickstart ```tsx [Browser] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. import type { Signer, Identifier } from '@xmtp/browser-sdk'; const signer: Signer = { type: 'EOA', getIdentifier: () => ({ identifier: '0x...', // Ethereum address as the identifier identifierKind: 'Ethereum', }), signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function }, }; // 2. Create the XMTP client import { Client } from '@xmtp/browser-sdk'; const client = await Client.create(signer, { // Note: dbEncryptionKey is not used for encryption in browser environments }); // 3. Start conversations const group = await client.conversations.newGroup( [bo.inboxId, caro.inboxId], createGroupOptions /* optional */ ); // 4. Send messages await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed], }); const allGroups = await client.conversations.listGroups({ consentStates: [ConsentState.Allowed], }); const allDms = await client.conversations.listDms({ consentStates: [ConsentState.Allowed], }); // Stream new messages const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { console.log('New message:', message); }, onError: (error) => { console.error(error); }, }); // Or use for-await loop for await (const message of stream) { console.log('New message:', message); } // Sync all new welcomes, preference updates, conversations, // and messages from allowed conversations await client.conversations.syncAll(['allowed']); ``` ## chat-apps/sdks/ios.mdx # Get started with the XMTP iOS SDK Use the [XMTP iOS SDK](https://github.com/xmtp/xmtp-ios) to build secure, private, and decentralized messaging into your iOS app. The guide provides some quickstart code, as well as a map to building a [secure chat app](/protocol/security) with XMTP, including support for: - End-to-end encrypted direct message and group chat conversations - Rich content types (attachments, reactions, replies, and more) - Spam-free chats using user consent preferences ## Quickstart ```swift [Swift] // 1. Create an EOA or SCW signer // Details depend on your app's wallet implementation. public struct EOAWallet: SigningKey { public var identity: PublicIdentity { return PublicIdentity(kind: .ethereum, identifier: key.publicAddress) } public var type: SignerType { .EOA } public func sign(message: String) async throws -> SignedData { return SignedData(try await key.sign(message: message)) } } // 2. Create the XMTP client let client = try await Client.create( account: SigningKey, options: ClientOptions.init( api: .init(env: .production, isSecure: true), dbEncryptionKey: keyBytes // 32 bytes ) ) // 3. Start conversations let group = try await client.conversations.newGroup([bo.inboxId, caro.inboxId]) let groupWithMeta = try await client.conversations.newGroup([bo.inboxId, caro.inboxId], permissionLevel: .admin_only, name: "The Group Name", imageUrl: "www.groupImage.com", description: "The description of the group" ) // 4. Send messages let dm = try await client.conversations.findOrCreateDm(with: recipientInboxId) try await dm.send(content: "Hello world") try await group.send(content: "Hello everyone") // 5. List, stream, and sync // List existing conversations let conversations = try await client.conversations.list() let filteredConversations = try await client.conversations.list(consentState: .allowed) // Stream new messages for await message in try await client.conversations.streamAllMessages(type: /* OPTIONAL .dms, .groups, .all */, consentState: [.allowed]) { // Received a message } // Sync all new welcomes, preference updates, conversations, // and messages from allowed conversations try await client.conversations.syncAllConversations(consentState: [.allowed]) ``` ## chat-apps/sdks/node.mdx # Get started with the XMTP Node SDK Use the [XMTP Node SDK](https://github.com/xmtp/xmtp-js/tree/main/sdks/node-sdk) to build agents and other server-side applications that interact with the XMTP network. :::tip[🤖 Building an agent?] For building AI agents, use the [Build an agent with XMTP](/agents/get-started/build-an-agent) tutorial tailored to this use case. ::: For all other server-side applications, including backends for chat apps, follow the get started guide below, which provides some quickstart code, as well as a map to building a [secure chat app](/protocol/security) with XMTP, including support for: - End-to-end encrypted direct message and group chat conversations - Rich content types (attachments, reactions, replies, and more) - Spam-free chats using user consent preferences ## Installation Install `@xmtp/node-sdk` as a dependency in your project. :::code-group ```bash [npm] npm i @xmtp/node-sdk ``` ```bash [pnpm] pnpm i @xmtp/node-sdk ``` ```bash [yarn] yarn add @xmtp/node-sdk ``` ```bash [bun] bun i @xmtp/node-sdk ``` ::: ## Quickstart ```tsx [Node] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const signer: Signer = { type: 'EOA', getIdentifier: () => ({ identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, }), signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function }, }; // 2. Create the XMTP client import { Client } from '@xmtp/node-sdk'; import { getRandomValues } from 'node:crypto'; const dbEncryptionKey = getRandomValues(new Uint8Array(32)); const client = await Client.create(signer, { dbEncryptionKey }); // 3. Start a new conversation const group = await client.conversations.newGroup( [bo.inboxId, caro.inboxId], createGroupOptions /* optional */ ); // 4. Send messages await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations const allConversations = await client.conversations.list({ consentStates: [ConsentState.Allowed], }); // Stream new messages const stream = await client.conversations.streamAllMessages({ consentStates: [ConsentState.Allowed], onValue: (message) => { console.log('New message:', message); }, onError: (error) => { console.error(error); }, }); // Or use for-await loop for await (const message of stream) { // Received a message console.log('New message:', message); } // Sync all new welcomes, preference updates, conversations, // and messages from allowed conversations await client.conversations.syncAll(['allowed']); ``` ## chat-apps/sdks/react-native.mdx # Get started with the XMTP React Native SDK Use the [XMTP React Native SDK](https://github.com/xmtp/xmtp-react-native) to build secure, private, and decentralized messaging into your cross-platform mobile app. The guide provides some quickstart code, as well as a map to building a [secure chat app](/protocol/security) with XMTP, including support for: - End-to-end encrypted direct message and group chat conversations - Rich content types (attachments, reactions, replies, and more) - Spam-free chats using user consent preferences ## Quickstart ```tsx [React Native] // 1. Create an EOA or SCW signer. // Details depend on your app's wallet implementation. export function convertEOAToSigner(eoaAccount: EOAAccount): Signer { return { getIdentifier: async () => new PublicIdentity(eoaAccount.address, 'ETHEREUM'), getChainId: () => undefined, getBlockNumber: () => undefined, signerType: () => 'EOA', signMessage: async (message: string) => ({ signature: await eoaAccount.signMessage(message), }), }; } // 2. Create the XMTP client const client = Client.create(signer, { env: 'production', dbEncryptionKey: keyBytes, // 32 bytes }); // 3. Start conversations const group = await client.conversations.newGroup([bo.inboxId, caro.inboxId]); const groupWithMeta = await client.conversations.newGroup( [bo.inboxId, caro.inboxId], { name: 'The Group Name', imageUrl: 'www.groupImage.com', description: 'The description of the group', permissionLevel: 'admin_only', } ); // 4. Send messages const dm = await client.conversations.findOrCreateDm(recipientInboxId); await dm.send('Hello world'); await group.send('Hello everyone'); // 5. List, stream, and sync // List existing conversations await client.conversations.list(['allowed']); // Stream new messages await client.conversations.streamAllMessages( async (message: DecodedMessage) => { // Received a message }, { consentState: ['allowed'] } ); // Sync all new welcomes, preference updates, conversations, // and messages from allowed conversations await client.conversations.syncAllConversations(['allowed']); ``` ## chat-apps/content-types/attachments.mdx --- description: Learn how to use the remote attachment, multiple remote attachment, or attachment content types to support attachments in your app built with XMTP --- # Support attachments in your app built with XMTP Use the remote attachment, multiple remote attachments, or attachment content type to support attachments in your app. - Use the [remote attachment content type](#support-remote-attachments-of-any-size) to send one remote attachment of any size. - Use the [multiple remote attachments content type](#support-multiple-remote-attachments-of-any-size) to send multiple remote attachments of any size. - Use the [attachment content type](#support-attachments-smaller-than-1mb) to send attachments smaller than 1MB. ## Support remote attachments of any size One remote attachment of any size can be sent in a message using the `RemoteAttachmentCodec` and a storage provider. To send multiple remote attachments of any size in a single message, see [Support multiple remote attachments of any size](#support-multiple-remote-attachments-of-any-size). ### Install the package In some SDKs, the `AttachmentCodec` is already included in the SDK. If not, you can install the package using the following command: :::code-group ```bash [npm] npm i @xmtp/content-type-remote-attachment ``` ```bash [yarn] yarn add @xmtp/content-type-remote-attachment ``` ```bash [pnpm] pnpm add @xmtp/content-type-remote-attachment ``` ::: ### Configure the content type After importing the package, you can register the codec. :::code-group ```jsx [Browser] import { AttachmentCodec, RemoteAttachmentCodec, } from '@xmtp/content-type-remote-attachment'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new AttachmentCodec(), new RemoteAttachmentCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { env: 'production', codecs: [new RemoteAttachmentCodec(), new StaticAttachmentCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.Attachment import org.xmtp.android.library.codecs.AttachmentCodec import org.xmtp.android.library.codecs.ContentTypeAttachment Client.register(codec = AttachmentCodec()) Client.register(codec = RemoteAttachmentCodec()) ``` ```swift [Swift] Client.register(AttachmentCodec()); Client.register(RemoteAttachmentCodec()); ``` ::: ### Send a remote attachment :::code-group
Load the file. This example uses a web browser to load the file: ```jsx //image is the uploaded event.target.files[0]; const data = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (reader.result instanceof ArrayBuffer) { resolve(reader.result); } else { reject(new Error('Not an ArrayBuffer')); } }; reader.readAsArrayBuffer(image); }); ``` Create an attachment object: ```tsx // Local file details const attachment = { filename: image?.name, mimeType: image?.type, data: new Uint8Array(data), }; ``` Use `RemoteAttachmentCodec.encodeEncrypted` to encrypt an attachment: ```tsx const encryptedEncoded = await RemoteAttachmentCodec.encodeEncrypted( attachment, new AttachmentCodec() ); ``` Upload an encrypted attachment to a location where it will be accessible via an HTTPS GET request. This location will depend on which storage provider you use based on your needs.

Now that you have a `url`, you can create a `RemoteAttachment`:

```jsx const remoteAttachment = { url: url, contentDigest: encryptedEncoded.digest, salt: encryptedEncoded.salt, nonce: encryptedEncoded.nonce, secret: encryptedEncoded.secret, scheme: 'https://', filename: attachment.filename, contentLength: attachment.data.byteLength, }; ``` Now that you have a remote attachment, you can send it: ```tsx await conversation.send(remoteAttachment, { contentType: ContentTypeRemoteAttachment, }); ```
This method takes a `DecryptedLocalAttachment` object as an argument: ```jsx const { encryptedLocalFileUri, metadata } = await alice.encryptAttachment({ fileUri: `file://${file}`, mimeType: 'text/plain', }); ``` Upload an encrypted file to a remote server: ```jsx let url = await uploadFile(encryptedLocalFileUri); ``` Send a remote attachment message: ```jsx await convo.send({ remoteAttachment: { ...metadata, scheme: 'https://', url, }, }); ```
Create an attachment object: ```kotlin [Kotlin] val attachment = Attachment( filename = "test.txt", mimeType = "text/plain", data = "hello world".toByteStringUtf8(), ) ``` Encode and encrypt an attachment for transport: ```kotlin [Kotlin] val encodedEncryptedContent = RemoteAttachment.encodeEncrypted( content = attachment, codec = AttachmentCodec(), ) ``` Create a remote attachment from an attachment: ```kotlin [Kotlin] val remoteAttachment = RemoteAttachment.from( encryptedEncodedContent = encodedEncryptedContent ) remoteAttachment.contentLength = attachment.data.size() remoteAttachment.filename = attachment.filename ``` Send a remote attachment and set the `contentType`: ```kotlin [Kotlin] val newConversation = client.conversations.newConversation(inboxId) newConversation.send( content = remoteAttachment, options = SendOptions(contentType = ContentTypeRemoteAttachment), ) ```
Create an attachment object: ```swift [Swift] let attachment = Attachment( filename: "screenshot.png", mimeType: "image/png", data: Data(somePNGData) ) ``` Encode and encrypt an attachment for transport: ```swift [Swift] // Encode an attachment and encrypt the encoded content const encryptedAttachment = try RemoteAttachment.encodeEncrypted( content: attachment, codec: AttachmentCodec() ) ``` Upload an encrypted attachment anywhere where it will be accessible via an HTTPS GET request. For example, you can use web3.storage: ```swift [Swift] func upload(data: Data, token: String): String { let url = URL(string: "https://api.web3.storage/upload")! var request = URLRequest(url: url) request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.addValue("XMTP", forHTTPHeaderField: "X-NAME") request.httpMethod = "POST" let responseData = try await URLSession.shared.upload(for: request, from: data).0 let response = try JSONDecoder().decode(Web3Storage.Response.self, from: responseData) return "https://\(response.cid).ipfs.w3s.link" } let url = upload(data: encryptedAttachment.payload, token: YOUR_WEB3_STORAGE_TOKEN) ``` Create a remote attachment from an attachment: ```swift [Swift] let remoteAttachment = try RemoteAttachment( url: url, encryptedEncodedContent: encryptedEncodedContent ) ``` Send a remote attachment and set the `contentType`: ```swift [Swift] try await conversation.send( content: remoteAttachment, options: .init( contentType: ContentTypeRemoteAttachment, fallback: "a description of the image" ) ) ```
::: ### Receive, decode, and decrypt a remote attachment Now that you can send a remote attachment, you need a way to receive it. For example: :::code-group
```tsx import { ContentTypeRemoteAttachment } from '@xmtp/content-type-remote-attachment'; if (message.contentType.sameAs(RemoteAttachmentContentType)) { const attachment = await RemoteAttachmentCodec.load(message.content, client); } ``` You now have the original attachment: ```bash [Bash] attachment.filename // => "screenshot.png" attachment.mimeType // => "image/png", attachment.data // => [the PNG data] ``` Once you've created the attachment object, you can create a preview to show in the message input field before sending: ```tsx const objectURL = URL.createObjectURL( new Blob([Buffer.from(attachment.data)], { type: attachment.mimeType, }) ); const img = document.createElement('img'); img.src = objectURL; img.title = attachment.filename; ```
On the receiving end, you can use the `decryptAttachment` method to decrypt the downloaded file. This method takes an `EncryptedLocalAttachment` object as an argument and returns a `DecryptedLocalAttachment` object. ```jsx if (message.contentTypeId === "xmtp.org/remoteStaticAttachment:1.0") { // Now we can decrypt the downloaded file using the message metadata. const attached = await xmtp_client.decryptAttachment({ encryptedLocalFileUri: downloadedFileUri, metadata: message.content() as RemoteAttachmentContent, }) //attached.filename //attached.mimeType //attached.fileUri } ``` Display the attachment: ```jsx ```
```kotlin [Kotlin] val message = newConversation.messages().first() val loadedRemoteAttachment: RemoteAttachment = messages.content() loadedRemoteAttachment.fetcher = Fetcher() runBlocking { val attachment: Attachment = loadedRemoteAttachment.load() } ```
```swift [Swift] let attachment: Attachment = try await remoteAttachment.content() ``` You now have the original attachment: ```swift [Swift] attachment.filename // => "screenshot.png" attachment.mimeType // => "image/png", attachment.data // => [the PNG data] ``` Once you've created the attachment object, you can create a preview to show in the message input field before sending: ```swift [Swift] import UIKIt import SwiftUI struct ContentView: View { var body: some View { Image(uiImage: UIImage(data: attachment.data)) } } ```
::: To handle unsupported content types, refer to the [fallback](/chat-apps/content-types/fallback) section. ## Support multiple remote attachments of any size Multiple remote attachments of any size can be sent in a single message using the `MultiRemoteAttachmentCodec` and a storage provider. ### Register necessary codecs :::code-group ```tsx [React Native] export const registerCodecs = () => { Client.register(new AttachmentCodec()); Client.register(new RemoteAttachmentCodec()); Client.register(new MultiRemoteAttachmentCodec()); }; ``` ```kotlin [Kotlin] Client.register(codec = AttachmentCodec()) Client.register(codec = RemoteAttachmentCodec()) Client.register(codec = MultiRemoteAttachmentCodec()) ``` ```swift [Swift] Client.register(codec: AttachmentCodec()) Client.register(codec: RemoteAttachmentCodec()) Client.register(codec: MultiRemoteAttachmentCodec()) ``` ::: ### Create multiple attachment objects Each attachment in the attachments array contains a URL that points to an encrypted `EncodedContent` object. The content must be accessible by an HTTP `GET` request to the URL. :::code-group ```ts [React Native] const attachment1: DecryptedLocalAttachment = { fileUri: 'content://media/external/images/media/image-1.png', mimeType: 'image/png', filename: 'image-1.png', }; const attachment2: DecryptedLocalAttachment = { fileUri: 'content://media/external/images/media/image-2.png', mimeType: 'image/png', filename: 'image-2.png', }; ``` ```kotlin [Kotlin] val attachment1 = Attachment( filename = "test1.txt", mimeType = "text/plain", data = "hello world".toByteStringUtf8(), ) val attachment2 = Attachment( filename = "test2.txt", mimeType = "text/plain", data = "hello world".toByteStringUtf8(), ) ``` ```swift [Swift] let attachment1 = Attachment( filename: "test1.txt", mimeType: "text/plain", data: Data("hello world".utf8) ) let attachment2 = Attachment( filename: "test2.txt", mimeType: "text/plain", data: Data("hello world".utf8) ) ``` ::: ### Encrypt and upload multiple attachments to a remote server :::code-group ```ts [React Native] const remoteAttachments: RemoteAttachmentInfo[] = []; for (const attachment of [attachment1, attachment2]) { // Encrypt the attachment and receive the local URI of the encrypted file const { encryptedLocalFileUri, metadata } = await alix.encryptAttachment(attachment); // Upload the attachment to a remote server and receive the URL // (Integrator must supply upload from local uri and return url functionality!) const url = uploadAttachmentForUrl(encryptedLocalFileUri); // Build the remote attachment info const remoteAttachmentInfo = MultiRemoteAttachmentCodec.buildMultiRemoteAttachmentInfo(url, metadata); remoteAttachments.push(remoteAttachmentInfo); } ``` ```kotlin [Kotlin] val attachmentCodec = AttachmentCodec() val remoteAttachmentInfos: MutableList = ArrayList() for (attachment: Attachment in listOf(attachment1, attachment2)) { // 1) Encode the attachment to raw bytes val encodedBytes = attachmentCodec.encode(attachment).toByteArray() // 2) Encrypt the bytes locally val encryptedAttachment = MultiRemoteAttachmentCodec.encryptBytesForLocalAttachment(encodedBytes, attachment.filename) // 3) "Upload" it, and get a url string back // (Integrator must supply upload from local uri and return url functionality!) val url = uploadEncryptedPayload(encryptedAttachment.payload.toByteArray()) // 4) Build a RemoteAttachmentInfo for that URL and encryptedAttachment val remoteAttachmentInfo = MultiRemoteAttachmentCodec.buildRemoteAttachmentInfo(encryptedAttachment, URL(url)) remoteAttachmentInfos.add(remoteAttachmentInfo) } ``` ```swift [Swift] var remoteAttachmentInfos: [MultiRemoteAttachment.RemoteAttachmentInfo] = [] for att in [attachment1, attachment2] { // 1) Encode the attachment to raw bytes let encodedBytes = try AttachmentCodec().encode(content: att).serializedData() // 2) Encrypt the bytes locally let encrypted = try MultiRemoteAttachmentCodec.encryptBytesForLocalAttachment(encodedBytes, filename: att.filename) // 3) "Upload" it, and get a url string back // (Integrator must supply upload from local uri and return url functionality!) let urlString = fakeUpload(encrypted.payload) // 4) Build a RemoteAttachmentInfo for that URL and encryptedAttachment let info = try MultiRemoteAttachmentCodec.buildRemoteAttachmentInfo( encryptedAttachment: encrypted, remoteUrl: URL(string: urlString)! ) remoteAttachmentInfos.append(info) } ``` ::: ### Send a message with multiple remote attachments :::code-group ```ts [React Native] await convo.send({ multiRemoteAttachment: { attachments: remoteAttachments, }, }); ``` ```kotlin [Kotlin] val multiRemoteAttachment = MultiRemoteAttachment(remoteAttachments = remoteAttachmentInfos.toList()) runBlocking { aliceConversation.send( content = multiRemoteAttachment, options = SendOptions(contentType = ContentTypeMultiRemoteAttachment), ) } ``` ```swift [Swift] let multiRemoteAttachment = MultiRemoteAttachment(remoteAttachments: remoteAttachmentInfos) let encodedContent = try MultiRemoteAttachmentCodec().encode(content: multiRemoteAttachment) try await alixConversation.send(encodedContent: encodedContent) ``` ::: ### Recognize and decode a multi remote attachment :::code-group ```ts [React Native] const messages = await conversation.messages(); if ( messages.size > 0 && messages[0].contentTypeId == 'xmtp.org/multiRemoteStaticContent:1.0' ) { // Decode the raw content back into a MultiRemoteAttachment const multiRemoteAttachment: MultiRemoteAttachment = messages[0].content(); // See next section for download, and decrypt the attachments } ``` ```kotlin [Kotlin] val messages = runBlocking { conversation.messages() } if (messages.size > 0 && messages[0].encodedContent.type.id.equals(ContentTypeMultiRemoteAttachment)) { // Decode the raw content back into a MultiRemoteAttachment val multiRemoteAttachment: FfiMultiRemoteAttachment = messages[0].content()!! // See next section for download, and decrypt the attachments } ``` ```swift [Swift] let messages = try await conversation.messages() if messages.size > 0 && messages[0].encodedContent.type.id.equals(ContentTypeMultiRemoteAttachment.id) { // Decode the raw content back into a MultiRemoteAttachment let multiRemoteAttachment: MultiRemoteAttachment = try messages[0].content() // See next section for download, and decrypt the attachments } ``` ::: ### Decode, download, and decrypt the attachments :::code-group ```ts [React Native] const decryptedAttachments: DecryptedLocalAttachment[] = []; for (const attachment of multiRemoteAttachment.attachments) { // Downloading the encrypted payload from the attachment URL and save the local file // (Integrator must supply download from url and save to local Uri functionality!) const encryptedFileLocalURIAfterDownload: string = downloadFromUrl( attachment.url ); // Decrypt the local file const decryptedLocalAttachment = await alix.decryptAttachment({ encryptedLocalFileUri: encryptedFileLocalURIAfterDownload, metadata: { secret: attachment.secret, salt: attachment.salt, nonce: attachment.nonce, contentDigest: attachment.contentDigest, filename: attachment.filename, } as RemoteAttachmentContent, }); decryptedAttachments.push(decryptedLocalAttachment); } ``` ```kotlin [Kotlin] val decryptedAttachments: MutableList = ArrayList() for (remoteAttachmentInfo: FfiRemoteAttachmentInfo in multiRemoteAttachment.attachments) { // convert to FfiRemoteAttachmentInfo to RemoteAttachment val remoteAttachment = RemoteAttachment( url = URL(remoteAttachmentInfo.url), filename = remoteAttachmentInfo.filename, contentDigest = remoteAttachmentInfo.contentDigest, nonce = remoteAttachmentInfo.nonce.toByteString(), scheme = remoteAttachmentInfo.scheme, salt = remoteAttachmentInfo.salt.toByteString(), secret = remoteAttachmentInfo.secret.toByteString(), contentLength = remoteAttachmentInfo.contentLength?.toInt(), ) // Download the encrypted payload (Integrator must supply download from url functionality!) val url = remoteAttachment.url.toString() val encryptedPayload: ByteArray = downloadFromUrl(url) // Combine encrypted payload with RemoteAttachment to create an EncryptedEncodedContent Object val encryptedAttachment: EncryptedEncodedContent = MultiRemoteAttachmentCodec.buildEncryptAttachmentResult(remoteAttachment, encryptedPayload) // Decrypt payload val encodedContent: EncodedContent = MultiRemoteAttachmentCodec.decryptAttachment(encryptedAttachment) // Convert EncodedContent to Attachment val attachment = attachmentCodec.decode(encodedContent) decryptedAttachments.add(attachment) } ``` ```swift [Swift] var decryptedAttachments: [Attachment] = [] for remoteAttachmentInfo in multiRemoteAttachment.remoteAttachments { // convert to RemoteAttachmentInfo to RemoteAttachment let remoteAttachment = try RemoteAttachment( url: remoteAttachmentInfo.url, contentDigest: remoteAttachmentInfo.contentDigest, secret: remoteAttachmentInfo.secret, salt: remoteAttachmentInfo.salt, nonce: remoteAttachmentInfo.nonce, scheme: RemoteAttachment.Scheme(rawValue: remoteAttachmentInfo.scheme) ?? .https, contentLength: Int(remoteAttachmentInfo.contentLength), filename: remoteAttachmentInfo.filename ) // Download the encrypted payload (Integrator must supply download from url functionality!) guard let encryptedPayload = downloadFromUrl(remoteAttachment.url) // Combine encrypted payload with RemoteAttachment to create an EncryptedEncodedContent Object let encryptedAttachment = MultiRemoteAttachmentCodec.buildEncryptAttachmentResult( remoteAttachment: remoteAttachment, encryptedPayload: downloadedPayload ) // Decrypt payload let encodedContent = try MultiRemoteAttachmentCodec.decryptAttachment(encryptedAttachment) // Convert EncodedContent to Attachment let attachment: Attachment = try encodedContent.decoded() decryptedAttachments.append(attachment) } ``` ::: ### Accessing the unencrypted attachments Use the file URIs in the decrypted attachments objects to display the attachments. :::code-group ```ts [React Native] // Example showing displaying attachments if they represent images const attachment1 = decryptedAttachments[0] const attachment2 = decryptedAttachments[1] ``` ```kotlin [Kotlin] // Example showing accessing filenames of final decrypted attachments assertEquals(decryptedAttachments[0].filename, "test1.txt") assertEquals(decryptedAttachments[1].filename, "test2.txt") ``` ```swift [Swift] // Example showing accessing filenames of final decrypted attachments XCTAssertEqual(decryptedAttachments[0].filename, "test1.txt") XCTAssertEqual(decryptedAttachments[1].filename, "test2.txt") ``` ::: ## Support attachments smaller than 1MB Attachments smaller than 1MB can be sent using the `AttachmentCodec`. The codec will automatically encrypt the attachment and upload it to the XMTP network. :::warning Unless a very specific use case we recommend using the [remote attachment content type](/chat-apps/content-types/attachments) instead since many attachments are larger than 1MB in a chat app. ::: ### Install the package In some SDKs, the `AttachmentCodec` is already included in the SDK. If not, you can install the package using the following command: :::code-group ```bash [npm] npm i @xmtp/content-type-remote-attachment ``` ```bash [yarn] yarn add @xmtp/content-type-remote-attachment ``` ```bash [pnpm] pnpm add @xmtp/content-type-remote-attachment ``` ::: ### Import and register :::code-group ```jsx [Browser] import { AttachmentCodec } from '@xmtp/content-type-remote-attachment'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new AttachmentCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { env: 'production', codecs: [new StaticAttachmentCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.Attachment import org.xmtp.android.library.codecs.AttachmentCodec import org.xmtp.android.library.codecs.ContentTypeAttachment Client.register(codec = AttachmentCodec()) ``` ```swift [Swift] Client.register(AttachmentCodec()); ``` ::: ### Load local file ```tsx // Read local file and extract its details const file = fs.readFileSync('xmtp.png'); const filename = path.basename('xmtp.png'); const extname = path.extname('xmtp.png'); console.log(`Filename: ${filename}`); console.log(`File Type: ${extname}`); ``` ### Send encrypted file ```tsx // Convert the file to a Uint8Array const blob = new Blob([file], { type: extname }); let imgArray = new Uint8Array(await blob.arrayBuffer()); const attachment = { filename: filename, mimeType: extname, //image, video or audio data: imgArray, }; console.log('Attachment created', attachment); await conversation.send(attachment, { contentType: ContentTypeAttachment }); ``` ### Receive encrypted file ```jsx if (message.contentType.sameAs(ContentTypeAttachment)) { const blobdecoded = new Blob([message.content.data], { type: message.content.mimeType, }); const url = URL.createObjectURL(blobdecoded); } ``` ## chat-apps/content-types/content-types.mdx # Understand content types with XMTP When you build an app with XMTP, all messages are encoded with a content type to ensure that an XMTP client knows how to encode and decode messages, ensuring interoperability and consistent display of messages across apps. In addition, message payloads are transported as a set of bytes. This means that payloads can carry any content type that a client supports, such as plain text, JSON, or even non-text binary or media content. At a high level, there are three categories of content types with XMTP: - Standard - Standards-track - Custom ## Standard content types A standard content type is one that has undergone the XMTP Request for Comment (XRC) process and has been adopted as an [XMTP Improvement Proposal](https://github.com/xmtp/XIPs#readme) (XIP). Once adopted, a standard content type is bundled in XMTP client SDKs. A developer can then import the standard content type from an SDK for use in their app. Here is the current standard content type: ### Text content type An app built with XMTP uses the `TextCodec` (plain text) standard content type by default. This means that if your app is sending plain text messages only, you don't need to perform any additional steps related to content types. ```jsx [Node] await conversation.send('gm'); ``` ## Standards-track content types A standards-track content type is one that's being actively reviewed for adoption as a standard content type through the XIP process. Here are standards-track content types that you can review, test, and adopt in your app today: - [Attachment content type](/chat-apps/content-types/attachments/#support-attachments-smaller-than-1mb): Use to send attachments smaller than 1MB. - [Remote attachment content type](/chat-apps/content-types/attachments): Use to send attachments of any size. - [Multiple remote attachments content type](/chat-apps/content-types/attachments#support-multiple-remote-attachments-of-any-size): Use to send attachments of any size. - [Read receipt content type](/chat-apps/content-types/read-receipts): Use to send a read receipt, which is a `timestamp` that indicates when a message was read. - [Reaction content type](/chat-apps/content-types/reactions): Use a reaction to send a quick and often emoji-based way to respond to a message. - [Reply content type](/chat-apps/content-types/replies): Use a reply to send a direct response to a specific message in a conversation. Users can select and reply to a particular message instead of sending a new one. - [Onchain transaction reference content type](/chat-apps/content-types/transaction-refs): Use to send references to onchain transactions, such as crypto payments. - [Onchain transaction content type](/chat-apps/content-types/transactions): Use to support sending transactions to a wallet for execution. ## chat-apps/content-types/custom.mdx --- description: Learn how to build custom content types --- # Build custom content types Any developer building with XMTP can create a custom content type and immediately start using it in their app. Unlike a standard content type, use of a custom content type doesn't require prerequisite formal adoption through the XRC and XIP processes. Building a custom content type enables you to manage data in a way that's more personalized or specialized to the needs of your app. For example, if you need a content type that isn't covered by a [standard](/chat-apps/content-types/content-types#standard-content-types) or [standards-track](/chat-apps/content-types/content-types#standards-track-content-types) content type, you can create a custom content type and begin using it immediately in your app. :::warning[warning] Be aware that your custom content type may not be automatically recognized or supported by other apps, which could result in the other apps overlooking or only displaying the fallback text for your custom content type. ::: Fallback plain text is "alt text"-like description text that you can associate with a custom content type if you are concerned that a receiving app might not be able to handle the content. If the receiving app is unable to handle the custom content, it displays the fallback plain text instead. If another app wants to display your custom content type, they must implement your custom content type in their code exactly as it's defined in your code. For more common content types, you can usually find a [standard](/chat-apps/content-types/content-types#standard-content-types) or [standards-track](/chat-apps/content-types/content-types#standards-track-content-types) content type to serve your needs. If your custom content type generates interest within the developer community, consider proposing it as a standard content type through the [XIP process](/protocol/xips). ## chat-apps/content-types/fallback.mdx # Use fallback text for content type compatibility When building with XMTP, you can't know in advance whether a recipient's app will support a given content type, especially a [custom one](/chat-apps/content-types/custom). Likewise, your own app might receive messages with content types it doesn't support. To prevent a poor user experience or app crashes, you should use the `fallback` property. **For sending:** When sending a message with a custom content type, always provide a `fallback` string. This string offers a human-readable representation of the content. If the recipient's app doesn't support your custom type, it can display the `fallback` text instead. To learn more, see [Build custom content types](/chat-apps/content-types/custom). **For receiving:** When your app receives a message, check if it supports the message's `contentType`. If not, render the `fallback` text. However, some content types, especially those not meant for display (like read receipts), won't have a `fallback`. In these `undefined` cases, you should generally ignore the message entirely. Displaying a generic "unsupported content" message for every silent background event would create a poor user experience and clutter the chat. The code examples below show how to handle both scenarios. :::code-group ```js [Browser] const codec = client.codecFor(content.contentType); if (!codec) { /*Not supported content type*/ if (message.fallback !== undefined) { return message.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```js [Node] const codec = client.codecFor(content.contentType); if (!codec) { /*Not supported content type*/ if (message.fallback !== undefined) { return message.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```jsx [React Native] //contentTypeID has the following structure `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}`; const isRegistered = message.contentTypeID in client.codecRegistry; if (!isRegistered) { // Not supported content type if (message?.fallback != null) { return message?.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```kotlin [Kotlin] val codec = client.codecRegistry.find(options?.contentType) if (!codec) { /*Not supported content type*/ if (message.fallback != null) { return message.fallback } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```swift [Swift] let codec = client.codecRegistry.find(for: contentType) if (!codec) { /*Not supported content type*/ if (message.fallback != null) { return message.fallback } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ::: ## chat-apps/content-types/reactions.mdx --- description: Learn how to use the reaction content type to support reactions in your app built with XMTP --- # Support reactions in your app built with XMTP Use the reaction content type to support reactions in your app. A reaction is a quick and often emoji-based way to respond to a message. Reactions are usually limited to a predefined set of emojis or symbols provided by the chat app. ## Use a local database for performance Use a local database to store reactions. This enables your app to performantly display a reaction with its [referenced message](#send-a-reaction) when rendering message lists. ### Install the package In some SDKs, the `ReactionCodec` is already included in the SDK. If not, you can install the package using the following command: :::code-group ```bash [npm] npm i @xmtp/content-type-reaction ``` ```bash [yarn] yarn add @xmtp/content-type-reaction ``` ```bash [pnpm] pnpm add @xmtp/content-type-reaction ``` ::: ## Configure the content type After importing the package, you can register the codec. :::code-group ```jsx [Browser] import { ReactionCodec } from '@xmtp/content-type-reaction'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new ReactionCodec()], }); ``` ```jsx [React Native] const client = await Client.create(signer, { env: 'production', codecs: [new ReactionCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.ReactionCodec Client.register(codec = ReactionCodec()) ``` ```swift [Swift] Client.register(ReactionCodec()); ``` ::: ## Send a reaction With XMTP, reactions are represented as objects with the following keys: - `reference`: ID of the message being reacted to - `action`: Action of the reaction (added or removed) - `content`: String representation of the reaction (smile, for example) to be interpreted by clients - `schema`: Schema of the reaction (Unicode, shortcode, or custom) :::code-group ```tsx [Browser] const reaction = { reference: someMessageID, action: 'added', content: 'smile', }; await conversation.send(reaction, { contentType: ContentTypeReaction, }); ``` ```jsx [React Native] // Assuming you have a conversation object and the ID of the message you're reacting to const reactionContent = { reaction: { reference: messageId, // ID of the message you're reacting to action: 'added', // Action can be 'added' or 'removed' schema: 'unicode', // Schema can be 'unicode', 'shortcode', or 'custom' content: '👍', // Content of the reaction }, }; await conversation.send(reactionContent); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.Reaction import org.xmtp.android.library.codecs.ReactionAction import org.xmtp.android.library.codecs.ReactionSchema import org.xmtp.android.library.codecs.ContentTypeReaction import org.xmtp.android.library.SendOptions val reaction = Reaction( reference = messageToReact.id, // the ID of the message you're reacting to action = ReactionAction.Added, // the action of the reaction content = "U+1F603", // the content of the reaction schema = ReactionSchema.Unicode // the schema of the reaction ) conversation.send( content = reaction, options = SendOptions(contentType = ContentTypeReaction) ) ``` ```swift [Swift] let reaction = Reaction( reference: messageToReact.id, action: .added, content: "U+1F603", schema: .unicode ) try await conversation.send( content: reaction, options: .init(contentType: ContentTypeReaction) ) ``` ::: ## Receive a reaction Now that you can send a reaction, you need a way to receive a reaction. For example: :::code-group ```tsx [Browser] if (message.contentType.sameAs(ContentTypeReaction)) { // We've got a reaction. const reaction: Reaction = message.content; } ``` ```jsx [React Native] if (message.contentTypeId === 'xmtp.org/reaction:1.0') { const reaction = message.content(); return reaction; //reaction.reference = id of the message being reacted to, //reaction.action = 'added', //reaction.schema = 'unicode', //reaction.content = '💖', } ``` ```kotlin [Kotlin] if (message.contentType == ContentTypeReaction) { // The message is a reaction val reactionCodec = ReactionCodec() val reaction: Reaction = reactionCodec.decode(message.content) } ``` ```swift [Swift] let content: Reaction = try message.content() ``` To handle unsupported content types, refer to the [fallback](/chat-apps/content-types/fallback) section. ## Display the reaction Generally, reactions should be interpreted as emoji. So, "smile" would translate to 😄 in UI clients. That being said, how you ultimately choose to render a reaction in your app is up to you. ## Notifications and reactions Reactions have `shouldPush` set to `false`, which means that reactions do not trigger push notifications as long as the notification server respects this flag. ## chat-apps/content-types/read-receipts.mdx --- description: Learn how to use the read receipt content type to support read receipts in your app built with XMTP --- # Support read receipts in your app built with XMTP Use the read receipt content type to support read receipts in your app. A read receipt is a `timestamp` that indicates when a message was read. It is sent as a message and can be used to calculate the time since the last message was read. ## Provide an opt-out option While this is a per-app decision, the best practice is to provide users with the option to opt out of sending read receipts. If a user opts out, when they read a message, a read receipt will not be sent to the sender of the message. ## Install the package :::code-group ```bash [npm] npm i @xmtp/content-type-read-receipt ``` ```bash [yarn] yarn add @xmtp/content-type-read-receipt ``` ```bash [pnpm] pnpm add @xmtp/content-type-read-receipt ``` ::: ## Configure the content type :::code-group ```js [Browser] import { ReadReceiptCodec } from '@xmtp/content-type-read-receipt'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new ReadReceiptCodec()], }); ``` ```js [React Native] const client = await Client.create(signer, { env: 'production', codecs: [new ReadReceiptCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.Client import org.xmtp.android.library.codecs.ReadReceiptCodec Client.register(codec = ReadReceiptCodec()) ``` ```swift [Swift] Client.register(codec: ReadReceiptCodec()) ``` ::: ## Send a read receipt :::code-group ```js [Browser] // The content of a read receipt message must be an empty object. await conversation.messages.send({}, ContentTypeReadReceipt); ``` ```jsx [React Native] await bobConversation.send({ readReceipt: {} }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.Client import org.xmtp.android.library.codecs.ReadReceipt import org.xmtp.android.library.codecs.ContentTypeReadReceipt import org.xmtp.android.library.messages.SendOptions // Create a ReadReceipt instance val readReceipt = ReadReceipt // Send the read receipt aliceConversation.send( content = readReceipt, options = SendOptions(contentType = ContentTypeReadReceipt), ) ``` ```swift [Swift] let read = ReadReceipt(timestamp: "2019-09-26T07:58:30.996+0200") try await conversation.send( content: read, options: .init(contentType: ContentTypeReadReceipt) ) ``` ::: ## Receive a read receipt Here's how you can receive a read receipt: :::code-group ```js [Browser] if (message.contentType.sameAs(ContentTypeReadReceipt)) { // The message is a read receipt const timestamp = message.sent; } ``` ```js [React Native] if (message.contentTypeId === 'xmtp.org/readReceipt:1.0') { return message.sent; //Date received } ``` ```kotlin [Kotlin] val message: DecodedMessage = conversation.messages().first() if (message.encodedContent.type == ContentTypeReadReceipt) { // The message is a ReadReceipt val readReceipt: ReadReceipt? = message.content() if (readReceipt != null) { println("Message read at: ${message.sent}") } } ``` ```swift [Swift] let content: ReadReceipt = try message.content() content.timestamp // "2019-09-26T07:58:30.996+0200" ``` ::: ## Display a read receipt `ReadReceipts` have an `undefined` or `nil` fallback, indicating the message is not expected to be displayed. To learn more, see [Handle unsupported content types](/chat-apps/content-types/fallback) section. ## Notifications and read receipts Read receipts have `shouldPush` set to `false`, which means that read receipts do not trigger push notifications as long as the notification server respects this flag. ## Use a read receipt Generally, a read receipt indicator should be displayed under the message it's associated with. The indicator can include a timestamp. Ultimately, how you choose to display a read receipt indicator is completely up to you. The read receipt is provided as an **empty message** whose timestamp provides the data needed for the indicators. **Be sure to filter out read receipt empty messages and not display them to users.** You can use a read receipt timestamp to calculate the time since the last message was read. While iterating through messages, you can be sure that the last message was read at the timestamp of the read receipt if the string of the timestamp is lower. ## chat-apps/content-types/replies.mdx --- description: Learn how to use the reply content type to support quote replies in your app built with XMTP --- # Support replies in your app built with XMTP Use the reply content type to support quote replies in your app. A reply is a method to directly respond to a specific message in a conversation. Users can select and reply to a particular message instead of sending a new one. ## Use a local database for performance Use a local database to store replies. This will enable your app to performantly display a reply with its [referenced message](#send-a-reply) when rendering message lists. ### Install the package In some SDKs, the `ReplyCodec` is already included in the SDK. If not, you can install the package using the following command: :::code-group ```bash [npm] npm i @xmtp/content-type-reply ``` ```bash [yarn] yarn add @xmtp/content-type-reply ``` ```bash [pnpm] pnpm add @xmtp/content-type-reply ``` ::: ## Configure the content type After importing the package, you can register the codec. :::code-group ```js [Browser] import { ReplyCodec } from '@xmtp/content-type-reply'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new ReplyCodec()], }); ``` ```js [React Native] const client = await Client.create(signer, { env: 'production', codecs: [new ReplyCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.ReplyCodec Client.register(codec = ReplyCodec()) ``` ```swift [Swift] Client.register(codec: ReplyCodec()) ``` ::: ## Send a reply Once you've created a reply, you can send it. Replies are represented as objects with two keys: - `reference`: ID of the message being replied to - `content`: String representation of the reply :::code-group ```ts [Browser] import { ContentTypeText } from '@xmtp/content-type-text'; import { ContentTypeReply } from '@xmtp/content-type-reply'; import type { Reply } from '@xmtp/content-type-reply'; const reply: Reply = { reference: someMessageID, contentType: ContentTypeText, content: 'I concur', }; await conversation.send(reply, { contentType: ContentTypeReply, }); ``` ```js [React Native] // Assuming you have a conversation object and the ID of the message you're replying to const replyContent = { reply: { reference: messageId, // ID of the message you're replying to content: { text: 'This is a reply', // Content of the reply }, }, }; await conversation.send(replyContent); ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.ContentTypeReply import org.xmtp.android.library.codecs.ContentTypeText import org.xmtp.android.library.codecs.Reply // Assuming aliceConversation and messageToReact are already defined val reply = Reply( reference = messageToReact.id, content = "Hello", contentType = ContentTypeText ) aliceConversation.send( content = reply, options = SendOptions(contentType = ContentTypeReply), ) ``` ```swift [Swift] let reply = Reply( reference: messageToReply.id, content: "Hello", contentType: ContentTypeText ) try await conversation.send( content: reply, options: .init(contentType: ContentTypeReply) ) ``` ::: ## Receive the content type :::code-group ```ts [Browser] if (message.contentType.sameAs(ContentTypeReply)) { // We've got a reply. const reply: Reply = message.content; } ``` ```jsx [React Native] if (message.contentTypeId === "xmtp.org/reply:1.0") { const reply = message.content(); if (reply) { const replyContent: ReplyContent = reply; const replyContentType = replyContent.contentType; const codec = client.codecRegistry[replyContentType]; const actualReplyContent = codec.decode(replyContent.content); } } ``` ```kotlin [Kotlin] if (encodedContent.type == ContentTypeReply) { // The message is a reply val reply: Reply? = message.content() println("Reply to: ${reply?.reference}") println("Content: ${reply?.content}") } ``` ```swift [Swift] let content: Reply = try message.content() ``` ::: To handle unsupported content types, refer to the [fallback](/chat-apps/content-types/fallback) section. ## Display the reply How you choose to display replies in your app is up to you. It might be useful to look at the user experience for replies in popular apps such as Telegram and Discord. For example, in Discord, users can reply to individual messages, and the reply provides a link to the original message. Note that the user experience of replies in iMessage and Slack follows more of a threaded pattern, where messages display in logical groupings, or threads. This reply content type doesn't support the threaded pattern. If you'd like to request support for a threaded reply pattern, [post an XIP idea](https://community.xmtp.org/c/development/ideas/54). ## chat-apps/content-types/transaction-refs.mdx --- description: Learn how to implement an onchain transaction reference content type --- # Support onchain transaction references in your app built with XMTP This package provides an XMTP content type to support onchain transaction references. It is a reference to an onchain transaction sent as a message. This content type facilitates sharing transaction hashes or IDs, thereby providing a direct link to onchain activities. Transaction references serve to display transaction details, facilitating the sharing of onchain activities, such as token transfers, between users. :::tip[Open for feedback] You're welcome to provide feedback by commenting on [XIP-21: Onchain transaction reference content type](https://community.xmtp.org/t/xip-21-on-chain-transaction-reference-content-type/532). ::: ## Install the package :::code-group ```bash [npm] npm i @xmtp/content-type-transaction-reference ``` ```bash [yarn] yarn add @xmtp/content-type-transaction-reference ``` ```bash [pnpm] pnpm add @xmtp/content-type-transaction-reference ``` ::: ## Configure the content type After importing the package, you can register the codec. :::code-group ```js [Browser] import { ContentTypeTransactionReference, TransactionReferenceCodec, } from '@xmtp/content-type-transaction-reference'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new TransactionReferenceCodec()], }); ``` ```kotlin [Kotlin] import org.xmtp.android.library.Client import org.xmtp.android.library.Signer import org.xmtp.android.library.codecs.ContentTypeTransactionReference import org.xmtp.android.library.codecs.TransactionReferenceCodec Client.register(codec = TransactionReferenceCodec()) // Create the XMTP client (assume account is a Signer) val xmtp = Client.create(account) ``` ```swift [Swift] import XMTP import XMTPContentTypeTransactionReference let codec = TransactionReferenceCodec() Client.register(codec: codec) // Create the XMTP client (assume account is a signer) let xmtp = try await Client.create(account: account) ``` ::: ## Send a transaction reference With XMTP, a transaction reference is represented as an object with the following keys: :::code-group ```ts [Browser] const transactionReference: TransactionReference = { /** * Optional namespace for the networkId */ namespace: "eip155", /** * The networkId for the transaction, in decimal or hexadecimal format */ networkId: 1; /** * The transaction hash */ reference: "0x123...abc"; /** * Optional metadata object */ metadata: { transactionType: "transfer", currency: "USDC", amount: 100000, // In integer format, this represents 1 USDC (100000/10^6) decimals: 6, // Specifies that the currency uses 6 decimal places fromAddress: "0x456...def", toAddress: "0x789...ghi" }; }; ``` ```kotlin [Kotlin] import org.xmtp.android.library.codecs.TransactionReference val transactionReference = TransactionReference( namespace = "eip155", networkId = "1", reference = "0x123...abc", metadata = TransactionReference.Metadata( transactionType = "transfer", currency = "USDC", amount = 100000.0, decimals = 6u, fromAddress = "0x456...def", toAddress = "0x789...ghi" ) ) ``` ```swift [Swift] import XMTPContentTypeTransactionReference let transactionReference = TransactionReference( namespace: "eip155", networkId: "1", reference: "0x123...abc", metadata: TransactionReference.Metadata( transactionType: "transfer", currency: "USDC", amount: 100_000, decimals: 6, fromAddress: "0x456...def", toAddress: "0x789...ghi" ) ) ``` ::: Once you have a transaction reference, you can send it as part of your conversation: :::code-group ```js [Browser] await conversation.messages.send(transactionReference, { contentType: ContentTypeTransactionReference, }); ``` ```kotlin [Kotlin] import kotlinx.coroutines.runBlocking import org.xmtp.android.library.SendOptions import org.xmtp.android.library.codecs.ContentTypeTransactionReference runBlocking { conversation.send( content = transactionReference, options = SendOptions(contentType = ContentTypeTransactionReference) ) } ``` ```swift [Swift] try await conversation.send( transactionReference, options: SendOptions(contentType: ContentTypeTransactionReference) ) ``` ::: ## Receive a transaction reference To receive and process a transaction reference, you can use the following code samples. To handle unsupported content types, refer to the [fallback](/chat-apps/content-types/fallback) section. :::code-group ```ts [Browser] // Assume `loadLastMessage` is a thing you have const message: DecodedMessage = await loadLastMessage(); if (!message.contentType.sameAs(ContentTypeTransactionReference)) { // Handle non-transaction reference message return; } const transactionRef: TransactionReference = message.content; // Process the transaction reference here ``` ```kotlin [Kotlin] // Assume message is a DecodedMessage if (message.type == ContentTypeTransactionReference) { val transactionRef: TransactionReference? = message.content() // Process the transaction reference here } ``` ```swift [Swift] if message.contentType == ContentTypeTransactionReference { if let transactionRef = message.content as? TransactionReference { // Process the transaction reference here } } ``` ::: ## Display the transaction reference Displaying a transaction reference typically involves rendering details such as the transaction hash, network ID, and any relevant metadata. Because the exact UI representation can vary based on your app's design, you might want to fetch onchain data before showing it to the user. ## chat-apps/content-types/transactions.mdx # Support onchain transactions in your app built with XMTP This package provides an XMTP content type to support sending transactions to a wallet for execution. Currently, this content type is supported in the Browser, Node, and React Native SDKs only. For an example of an agent that implements the transaction content type, see [xmtp-transactions](https://github.com/xmtplabs/xmtp-agent-examples/tree/main/examples/xmtp-transactions). :::tip[Open for feedback] You are welcome to provide feedback on this implementation by commenting on [XIP-59: Trigger on-chain calls via wallet_sendCalls](https://community.xmtp.org/t/xip-59-trigger-on-chain-calls-via-wallet-sendcalls/889). ::: ## Install the package :::code-group ```bash [npm] npm i @xmtp/content-type-wallet-send-calls ``` ```bash [yarn] yarn add @xmtp/content-type-wallet-send-calls ``` ```bash [pnpm] pnpm i @xmtp/content-type-wallet-send-calls ``` ::: ## Configure the content type After importing the package, you can register the codec. ```js [Browser] import { WalletSendCallsCodec } from '@xmtp/content-type-wallet-send-calls'; // Create the XMTP client const xmtp = await Client.create(signer, { env: 'dev', codecs: [new WalletSendCallsCodec()], }); ``` ## Create a transaction request With XMTP, a transaction request is represented using `wallet_sendCalls` with additional metadata for display. ```ts [TypeScript] const walletSendCalls: WalletSendCallsParams = { version: '1.0', from: '0x123...abc', chainId: '0x2105', calls: [ { to: '0x456...def', value: '0x5AF3107A4000', metadata: { description: 'Send 0.0001 ETH on base to 0x456...def', transactionType: 'transfer', currency: 'ETH', amount: 100000000000000, decimals: 18, toAddress: '0x456...def', }, }, { to: '0x789...cba', data: '0xdead...beef', metadata: { description: 'Lend 10 USDC on base with Morpho @ 8.5% APY', transactionType: 'lend', currency: 'USDC', amount: 10000000, decimals: 6, platform: 'morpho', apy: '8.5', }, }, ], }; ``` ## Send a transaction request Once you have a transaction reference, you can send it as part of your conversation: ```ts [TypeScript] await conversation.messages.send(walletSendCalls, { contentType: ContentTypeWalletSendCalls, }); ``` ## Receive a transaction request To receive and process a transaction request: ```ts [TypeScript] // Assume `loadLastMessage` is a thing you have const message: DecodedMessage = await loadLastMessage(); if (!message.contentType.sameAs(ContentTypeWalletSendCalls)) { // Handle non-transaction request message return; } const walletSendCalls: WalletSendCallsParams = message.content; // Process the transaction request here ``` ## chat-apps/core-messaging/create-a-client.mdx # Create an XMTP client Create an XMTP client that can use the signing capabilities provided by the [signer](/chat-apps/core-messaging/create-a-signer). This signer links the client to the appropriate EOA or SCW. ## Understand creating and building a client This video provides a walkthrough of creating and building a client. ### How it works When you call `Client.create()`, the following steps happen under the hood: 1. Extracts the `signer` and retrieves the wallet address from it. 2. Checks the XMTP identity ledger to find an inbox ID associated with the signer address. The inbox ID serves as the user's identity on the XMTP network. 1. If it doesn't find an existing inbox ID, it requests a wallet signature to register the identity and create an inbox ID. 2. If it finds an existing inbox ID, it uses the existing inbox ID. 3. Checks if a local SQLite database exists. This database contains the identity's installation state and message data. 1. If it doesn't find an existing local database, it creates one. On non-web platforms, it encrypts the database with the provided `dbEncryptionKey`. 2. If it finds an existing local database: - **For the Node, React Native, Android, and iOS SDKs**: It checks if the provided `dbEncryptionKey` matches. If it matches, it uses the existing database. If not, it creates a new database encrypted with the provided key. - **For the Browser SDK**: A `dbEncryptionKey` is not used for encryption due to technical limitations in web environments. Be aware that the database is not encrypted. 4. Returns the XMTP client, ready to send and receive messages. ### Keep the database encryption key safe The `dbEncryptionKey` client option is used by the Node, React Native, Android, and Swift SDKs only. The encryption key is critical to the stability and continuity of an XMTP client. It encrypts the local SQLite database created when you call `Client.create()`, and must be provided every time you create or build a client. As long as the local database and encryption key remain intact, you can use [`Client.build()`](#build-an-existing-client) to rehydrate the same client without re-signing. This encryption key is not stored or persisted by the XMTP SDK, so it's your responsibility as the app developer to store it securely and consistently. If the encryption key is lost, rotated, or passed incorrectly during a subsequent `Client.create()` or `Client.build()` call (on non-web platforms), the app will be unable to access the local database. Likewise, if you initially provided the `dbPath` option, you must always provide it with every subsequent call or the client may be unable to access the database. The client will assume that the database can't be decrypted or doesn't exist, and will fall back to creating a new installation. Creating a new installation requires a new identity registration and signature—and most importantly, **results in loss of access to all previously stored messages** unless the user has done a [history sync](/chat-apps/list-stream-sync/history-sync). To ensure seamless app experiences persist the `dbEncryptionKey` securely, and make sure it's available and correctly passed on each app launch The `dbEncryptionKey` client option is not used by the Browser SDK for due to technical limitations in web environments. In this case, be aware that the database is not encrypted. To learn more about database operations, see the [XMTP MLS protocol spec](https://github.com/xmtp/libxmtp/blob/main/xmtp_mls/README.md). ### Database encryption key loss during iOS device transfers When you transfer data to a new iOS device, the local database file may be moved without the encryption key, causing decryption errors. This commonly occurs when users choose Apple's direct transfer option during new device setup, as Apple aggressively moves files to the new device. To prevent this issue, exclude the database directory from backups and device transfers. For example, if you set a custom `dbPath` to a known directory, you can mark it as excluded from backups in iOS: ```swift func addSkipBackupAttribute(var folder: URL) throws { try folder.setResourceValue(true, forKey: .isExcludedFromBackupKey) } ``` To learn more about this function, see [isExcludedFromBackupKey](https://developer.apple.com/documentation/foundation/urlresourcekey/isexcludedfrombackupkey) in Apple's documentation. ### View an encrypted database For debugging, it can be useful to decrypt a locally stored database. When a `dbEncryptionKey` is used, the XMTP client creates a [SQLCipher database](https://www.zetetic.net/sqlcipher/) which applies transparent 256-bit AES encryption. A `.sqlitecipher_salt` file is also generated alongside the database. To open this database, you need to construct the password by prefixing `0x` (to indicate hexadecimal numbers), then appending the encryption key (64 hex characters, 32 bytes) and the salt (32 hex characters, 16 bytes). For example, if your encryption key is `A` and your salt is `B`, the resulting password would be `0xAB`. The database also uses a [plaintext header size](https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size) of 32 bytes. If you want to inspect the database visually, you can use [DB Browser for SQLite](https://sqlitebrowser.org/), an open source tool that supports SQLite and SQLCipher. In its **Custom** encryption settings, set the **Plaintext Header Size** to **_32_**, and use the full **Password** as a **Raw key**: ![DB Browser for SQLite](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/db-browser-sqlite.png) ## Create a client To call `Client.create()`, you must pass in a required `signer` and can also pass in any of the optional parameters covered in [Configure an XMTP client](#configure-an-xmtp-client). :::code-group ```tsx [Browser] import { Client, type Signer } from '@xmtp/browser-sdk'; // create a signer const signer: Signer = { /* ... */ }; const client = await Client.create( signer, // client options { // Note: dbEncryptionKey is not used for encryption in browser environments } ); ``` ```tsx [Node] import { Client, type Signer } from '@xmtp/node-sdk'; import { getRandomValues } from 'node:crypto'; // create a signer const signer: Signer = { /* ... */ }; /** * The database encryption key is optional but strongly recommended for * secure local storage of the database. * * This value must be consistent when creating a client with an existing * database. * * You can provide the key as either: * - Uint8Array (32 bytes) * - Hex string with 0x prefix (64 hex characters representing 32 bytes) */ const dbEncryptionKey = `0x${getRandomValues(new Uint8Array(32))}`; const client = await Client.create( signer, // client options { dbEncryptionKey, // Optional: Use a function to dynamically set the database path based on inbox ID // dbPath: (inboxId) => `./databases/xmtp-${inboxId}.db3`, } ); ``` ```tsx [React Native] Client.create(signer, { env: 'production', // 'local' | 'dev' | 'production' dbEncryptionKey: keyBytes, // 32 bytes }); ``` ```kotlin [Kotlin] val options = ClientOptions( ClientOptions.Api(XMTPEnvironment.PRODUCTION, true), appContext = ApplicationContext(), dbEncryptionKey = keyBytes // 32 bytes ) val client = Client().create( account = SigningKey, options = options ) ``` ```swift [Swift] let options = ClientOptions.init( api: .init(env: .production, isSecure: true), dbEncryptionKey: keyBytes // 32 bytes ) let client = try await Client.create( account: SigningKey, options: options ) ``` ::: ### Configure an XMTP client You can configure an XMTP client with these options passed to `Client.create`: :::code-group ```tsx [Browser] import type { ContentCodec } from '@xmtp/content-type-primitives'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ env?: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. * * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` * * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, * and required upgrades. */ appVersion?: string; /** * apiUrl can be used to override the `env` flag and connect to a * specific endpoint */ apiUrl?: string; /** * historySyncUrl can be used to override the `env` flag and connect to a * specific endpoint for syncing history */ historySyncUrl?: string | null; /** * Allow configuring codecs for additional content types */ codecs?: ContentCodec[]; /** * Path to the local DB * * There are 4 value types that can be used to specify the database path: * * - `undefined` (or excluded from the client options) * The database will be created in the current working directory and is based on * the XMTP environment and client inbox ID. * Example: `xmtp-dev-.db3` * * - `null` * No database will be created and all data will be lost once the client disconnects. * * - `string` * The given path will be used to create the database. * Example: `./my-db.db3` * * - `function` * A callback function that receives the inbox ID and returns a string path. * Example: `(inboxId) => string` */ dbPath?: string | null | ((inboxId: string) => string); /** * Encryption key for the local DB (32 bytes) * * Accepts either: * - Uint8Array (32 bytes) * - Hex string with 0x prefix (64 hex characters representing 32 bytes) */ dbEncryptionKey?: Uint8Array | `0x${string}`; /** * Enable structured JSON logging */ structuredLogging?: boolean; /** * Enable performance metrics */ performanceLogging?: boolean; /** * Logging level */ loggingLevel?: 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace'; /** * Disable automatic registration when creating a client */ disableAutoRegister?: boolean; /** * Disable device sync */ disableDeviceSync?: boolean; }; ``` ```tsx [Node] import type { ContentCodec } from '@xmtp/content-type-primitives'; import type { LogLevel } from '@xmtp/node-bindings'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ env?: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. * * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` * * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, * and required upgrades. */ appVersion?: string; /** * apiUrl can be used to override the `env` flag and connect to a * specific endpoint */ apiUrl?: string; /** * historySyncUrl can be used to override the `env` flag and connect to a * specific endpoint for syncing history */ historySyncUrl?: string | null; /** * Path to the local DB * * There are 4 value types that can be used to specify the database path: * * - `undefined` (or excluded from the client options) * The database will be created in the current working directory and is based on * the XMTP environment and client inbox ID. * Example: `xmtp-dev-.db3` * * - `null` * No database will be created and all data will be lost once the client disconnects. * * - `string` * The given path will be used to create the database. * Example: `./my-db.db3` * * - `function` * A callback function that receives the inbox ID and returns a string path. * Example: `(inboxId) => string` */ dbPath?: string | null | ((inboxId: string) => string); /** * Encryption key for the local DB (32 bytes) * * Accepts either: * - Uint8Array (32 bytes) * - Hex string with 0x prefix (64 hex characters representing 32 bytes) */ dbEncryptionKey?: Uint8Array | `0x${string}`; /** * Allow configuring codecs for additional content types */ codecs?: ContentCodec[]; /** * Enable structured JSON logging */ structuredLogging?: boolean; /** * Logging level */ loggingLevel?: LogLevel; /** * Disable automatic registration when creating a client */ disableAutoRegister?: boolean; /** * Disable device sync */ disableDeviceSync?: boolean; }; ``` ```tsx [React Native] import type { ContentCodec } from '@xmtp/react-native-sdk'; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ env: 'local' | 'dev' | 'production'; /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. * * You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. * * For example: `appVersion: 'alix/2.x'` * * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you with app * support, especially around communicating important SDK updates, deprecations, * and required upgrades. */ appVersion?: string; /** * REQUIRED specify the encryption key for the database. The encryption key must be exactly 32 bytes. */ dbEncryptionKey: Uint8Array; /** * Set optional callbacks for handling identity setup */ preAuthenticateToInboxCallback?: () => Promise | void; /** * OPTIONAL specify the XMTP managed database directory */ dbDirectory?: string; /** * OPTIONAL specify a url to sync message history from */ historySyncUrl?: string; /** * OPTIONAL specify a custom local host for testing on physical devices for example `localhost` */ customLocalHost?: string; /** * Allow configuring codecs for additional content types */ codecs?: ContentCodec[]; }; ``` ```kotlin [Kotlin] import android.content.Context typealias PreEventCallback = suspend () -> Unit data class ClientOptions( val api: Api = Api(), val preAuthenticateToInboxCallback: PreEventCallback? = null, val appContext: Context, val dbEncryptionKey: ByteArray, val historySyncUrl: String? = when (api.env) { XMTPEnvironment.PRODUCTION -> "https://message-history.production.ephemera.network/" XMTPEnvironment.LOCAL -> "http://0.0.0.0:5558" else -> "https://message-history.dev.ephemera.network/" }, val dbDirectory: String? = null, ) { data class Api( val env: XMTPEnvironment = XMTPEnvironment.DEV, val isSecure: Boolean = true, /** * Add a client app version identifier that's included with API requests. * Production apps are strongly encouraged to set this value. * * You can use the following format: `appVersion: "APP_NAME/APP_VERSION"`. * * For example: `appVersion: 'alix/2.x'` * * If you have an app and an agent, it's best to distinguish them from each other by * adding `-app` and `-agent` to the names. For example: * * - App: `appVersion: 'alix-app/3.x'` * - Agent: `appVersion: 'alix-agent/2.x'` * * Setting this value provides telemetry that shows which apps are using the * XMTP client SDK. This information can help XMTP core developers provide you * with app support, especially around communicating important SDK updates, * deprecations, and required upgrades. */ val appVersion: String? = null, ) } ``` ```swift [Swift] import LibXMTP public struct ClientOptions { // Specify network options public struct Api { /// Specify which XMTP network to connect to. Defaults to ``.dev`` public var env: XMTPEnvironment = .dev /// Specify whether the API client should use TLS security. In general this should only be false when using the `.local` environment. public var isSecure: Bool = true /// Add a client app version identifier that's included with API requests. /// Production apps are strongly encouraged to set this value. /// /// You can use the following format: `appVersion: "APP_NAME/APP_VERSION"`. /// /// For example: `appVersion: 'alix/2.x'` /// /// If you have an app and an agent, it's best to distinguish them from each other by /// adding `-app` and `-agent` to the names. For example: /// - App: `appVersion: 'alix-app/3.x'` /// - Agent: `appVersion: 'alix-agent/2.x'` /// /// Setting this value provides telemetry that shows which apps are using the /// XMTP client SDK. This information can help XMTP core developers provide you // with app support, especially around communicating important SDK updates, /// deprecations, and required upgrades. public var appVersion: String? } public var api = Api() public var codecs: [any ContentCodec] = [] /// `preAuthenticateToInboxCallback` will be called immediately before an Auth Inbox signature is requested from the user public var preAuthenticateToInboxCallback: PreEventCallback? public var dbEncryptionKey: Data public var dbDirectory: String? public var historySyncUrl: String? } ``` ::: ### Set the `appVersion` client option Be sure to set the `appVersion` client option for your production app. You can use the following format: `appVersion: 'APP_NAME/APP_VERSION'`. For example: `appVersion: 'alix/2.x'` If you have an app and an agent, it's best to distinguish them from each other by adding `-app` and `-agent` to the names. For example: - App: `appVersion: 'alix-app/3.x'` - Agent: `appVersion: 'alix-agent/2.x'` The `appVersion` value is included with API requests to provide telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP core developers provide you with app support, especially around communicating important SDK updates, deprecations, and required upgrades. ### XMTP network environments XMTP provides `dev` and `production` network environments. These networks are completely separate and not interchangeable. For example, an XMTP identity on the `dev` network is completely distinct from the XMTP identity on the `production` network, as are the messages associated with these identities. In addition, XMTP identities and messages created on the `dev` network can't be accessed from or moved to the `production` network, and vice versa. :::tip When you create a client, it connects to the XMTP `dev` network by default. Set your client's network environment using the appropriate [client option](#configure-an-xmtp-client). ::: The `production` network is configured to store messages indefinitely. XMTP may occasionally delete messages and identities from the `dev` network, and will provide advance notice in the [XMTP Community Forums](https://community.xmtp.org/). You can use a `local` network environment to have a client communicate with an XMTP node you are running locally. During development, using `local` is a great option for speed and reliability. Use the [xmtp-local-node](https://github.com/xmtp/xmtp-local-node/tree/main) repo to easily run a local XMTP node. ## Build an existing client Build, or resume, an existing client (created using [`Client.create()`](#create-a-client)) that's logged in and has an existing local database. For React Native, Android, and iOS SDKs, when building a client with an existing `inboxId`, the client automatically operates in offline mode since no network request is needed to check the identity ledger. In offline mode, the client: - Skips all network requests (preference syncing between installations, validating inbox, etc.) - Works entirely from the local database - Can be synchronized later with `syncAllConversations()` or by recreating the client without the offline flag. :::code-group ```tsx [Browser] import { Client, type Identifier } from '@xmtp/browser-sdk'; const identifier: Identifier = { identifier: '0x1234567890abcdef1234567890abcdef12345678', identifierKind: 'Ethereum', }; const client = await Client.build(identifier, options); ``` ```tsx [Node] import { Client, IdentifierKind, type Identifier } from '@xmtp/node-sdk'; const identifier: Identifier = { identifier: '0x1234567890abcdef1234567890abcdef12345678', identifierKind: IdentifierKind.Ethereum, }; const client = await Client.build(identifier, options); ``` ```tsx [React Native] Client.build(identity, { env: 'production', // 'local' | 'dev' | 'production' dbEncryptionKey: keyBytes, // 32 bytes }); ``` ```kotlin [Kotlin] val options = ClientOptions( ClientOptions.Api(XMTPEnvironment.PRODUCTION, true), appContext = ApplicationContext(), dbEncryptionKey = keyBytes ) val client = Client().build( identity = identity, options = options ) ``` ```swift [Swift] let options = ClientOptions.init( api: .init(env: .production, isSecure: true), dbEncryptionKey: keyBytes // 32 bytes ) let client = try await Client.build( identity: identity, options: options ) ``` ::: ## Log out a client When you log a user out of your app, you can give them the option to delete their local database. :::tip[Important] If the user chooses to delete their local database, they will lose all of their messages and will have to create a new installation the next time they log in. ::: :::code-group ```tsx [Browser] /** * The Browser SDK client does not currently support deleting the local database. */ // this method only terminates the client's associated web worker client.close(); ``` ```tsx [Node] /** * The Node SDK client does not have a method to delete the local database. * Simply delete the local database file from the file system. */ ``` ```tsx [React Native] await client.deleteLocalDatabase(); await Client.dropClient(client.installationId); ``` ```kotlin [Kotlin] client.deleteLocalDatabase() ``` ```swift [Swift] try await client.deleteLocalDatabase() ``` ::: ## chat-apps/core-messaging/create-a-signer.mdx # Create a EOA or SCW signer XMTP SDKs support message signing with 2 different types of Ethereum accounts: Externally Owned Accounts (EOAs) and Smart Contract Wallets (SCWs). All SDK clients accept a signer object (or instance), which provides a method for signing messages. ## Create an Externally Owned Account signer The EOA signer must have 3 properties: the account type, a function that returns the account identifier, and a function that signs messages. :::code-group ```tsx [Browser] import type { Signer, Identifier } from '@xmtp/browser-sdk'; const accountIdentifier: Identifier = { identifier: '0x...', // Ethereum address as the identifier identifierKind: 'Ethereum', // Specifies the identity type }; const signer: Signer = { type: 'EOA', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function }, }; ``` ```tsx [Node] import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { type: 'EOA', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function }, }; ``` ```tsx [React Native] // Example EOA Signer export function convertEOAToSigner(eoaAccount: EOAAccount): Signer { return { getIdentifier: async () => new PublicIdentity(eoaAccount.address, 'ETHEREUM'), getChainId: () => undefined, // Provide a chain ID if available or return undefined getBlockNumber: () => undefined, // Block number is typically not available in Wallet, return undefined signerType: () => 'EOA', // "EOA" indicates an externally owned account signMessage: async (message: string) => { const signature = await eoaAccount.signMessage(message); return { signature, }; }, }; } ``` ```kotlin [Kotlin] class EOAWallet : SigningKey { override val publicIdentity: PublicIdentity get() = PublicIdentity( IdentityKind.ETHEREUM, key.publicAddress ) override val type: SignerType get() = SignerType.EOA override suspend fun sign(message: String): SignedData { val signature = key.sign(message = message) return SignedData(signature) } } ``` ```swift [Swift] public struct EOAWallet: SigningKey { public var identity: PublicIdentity { return PublicIdentity(kind: .ethereum, identifier: key.publicAddress) } public var type: SignerType { .EOA } public func sign(message: String) async throws -> SignedData { let signature = try await key.sign(message: message) return SignedData(signature) } } ``` ::: ## Create a Smart Contract Wallet signer The SCW signer has the same 3 required properties as the EOA signer, but also requires a function that returns the chain ID of the blockchain being used and an optional function that returns the block number to verify signatures against. If a function is not provided to retrieve the block number, the latest block number will be used. Here is a list of supported chain IDs: - chain_rpc_1 = string - chain_rpc_8453 = string - chain_rpc_42161 = string - chain_rpc_10 = string - chain_rpc_137 = string - chain_rpc_324 = string - chain_rpc_59144 = string - chain_rpc_480 = string Need support for a different chain ID? Please post your request to the [XMTP Community Forums](https://community.xmtp.org/c/general/ideas/54). The details of creating an SCW signer are highly dependent on the wallet provider and the library you're using to interact with it. Here are some general guidelines to consider: - **Wallet provider integration**: Different wallet providers (Safe, Argent, Rainbow, etc.) have different methods for signing messages. See the wallet provider documentation for more details. - **Library selection**: Choose a library that supports your wallet provider (e.g., viem, ethers.js, web3.js). Each library has its own API for interacting with wallets. See the library documentation for more details. - **Add an Ethereum-specific prefix**: Before signing, Ethereum requires a specific prefix to be added to the message. To learn more, see [ERC-191: Signed Data Standard](https://eips.ethereum.org/EIPS/eip-191). Libraries and wallet providers might add the prefix for you, so make sure you don't add the prefix twice. - **Hash the prefixed message with Keccak-256**: The prefixed message is hashed using the Keccak-256 algorithm, which is Ethereum's standard hashing algorithm. This step creates a fixed-length representation of the message, ensuring consistency and security. Note that some wallet providers might handle this hashing internally. - **Sign the replay-safe hash**: The replay-safe hash is signed using the private key of the SCW. This generates a cryptographic signature that proves ownership of the wallet and ensures the integrity of the message. - **Convert the signature to a Uint8Array**: The resulting signature is converted to a `Uint8Array` format, which is required by the XMTP SDK for compatibility and further processing. The code snippets below are examples only and will need to be adapted based on your specific wallet provider and library. :::code-group ```tsx [Browser] export const createSCWSigner = ( address: `0x${string}`, walletClient: WalletClient, chainId: bigint, ): Signer => { return { type: "SCW", getIdentifier: () => ({ identifier: address.toLowerCase(), identifierKind: "Ethereum", }), signMessage: async (message: string) => { const signature = await walletClient.signMessage({ account: address, message, }); return toBytes(signature); }, getChainId: () => { return chainId; }, }; ``` ```tsx [Node] import type { Signer, Identifier, IdentifierKind } from '@xmtp/node-sdk'; const accountIdentifier: Identifier = { identifier: '0x...', // Ethereum address as the identifier identifierKind: IdentifierKind.Ethereum, // Specifies the identity type }; const signer: Signer = { type: 'SCW', getIdentifier: () => accountIdentifier, signMessage: async (message: string): Uint8Array => { // typically, signing methods return a hex string // this string must be converted to bytes and returned in this function }, getChainId: () => BigInt(8453), // Example: Base chain ID }; ``` ```tsx [React Native] // Example SCW Signer export function convertSCWToSigner(scwAccount: SCWAccount): Signer { return { getIdentifier: async () => new PublicIdentity(scwAccount.address, 'ETHEREUM'), getChainId: () => 8453, // https://chainlist.org/ getBlockNumber: () => undefined, // Optional: will be computed at runtime signerType: () => 'SCW', // "SCW" indicates smart contract wallet account signMessage: async (message: string) => { const byteArray = await scwAccount.signMessage(message); const signature = ethers.utils.hexlify(byteArray); // Convert to hex string return { signature, }; }, }; } ``` ```kotlin [Kotlin] class SCWallet : SigningKey { override val publicIdentity: PublicIdentity get() = PublicIdentity( IdentityKind.ETHEREUM, key.publicAddress ) override val type: SignerType get() = SignerType.SCW override var chainId: Long? = 8453 // https://chainlist.org/ override var blockNumber: Long? = null // Optional: will be computed at runtime override suspend fun sign(message: String): SignedData { val signature = key.sign(message = message) return SignedData(signature) } } ``` ```swift [Swift] public struct SCWallet: SigningKey { public var identity: PublicIdentity { return PublicIdentity(kind: .ethereum, identifier: key.publicAddress) } public var chainId: Int64? { 8453 } public var blockNumber: Int64? { nil } public var type: SignerType { .SCW } public func sign(message: String) async throws -> SignedData { let signature = try await key.sign(message: message) return SignedData(signature.hexStringToByteArray ) } } ``` ::: ## chat-apps/core-messaging/create-conversations.mdx # Create conversations ## Check if an identity is reachable The first step to creating a conversation is to verify that participants' identities are reachable on XMTP. The `canMessage` method checks each identity's compatibility, returning a response indicating whether each identity can receive messages. Once you have the verified identities, you can create a new conversation, whether it's a group chat or direct message (DM). :::code-group ```js [Browser] import { Client, type Identifier } from "@xmtp/browser-sdk"; const identifiers: Identifier[] = [ { identifier: "0xboAddress", identifierKind: "Ethereum" }, { identifier: "0xcaroAddress", identifierKind: "Ethereum" } ]; // response is a Map of string (identifier) => boolean (is reachable) const response = await Client.canMessage(identifiers); ``` ```js [Node] import { Client, IdentifierKind, type Identifier } from "@xmtp/node-sdk"; const identifiers: Identifier[] = [ { identifier: "0xboAddress", identifierKind: IdentifierKind.Ethereum }, { identifier: "0xcaroAddress", identifierKind: IdentifierKind.Ethereum } ]; // response is a Map of string (identifier) => boolean (is reachable) const response = await Client.canMessage(identifiers); ``` ```tsx [React Native] // Request const canMessage = await client.canMessage([ boIdentity, v2OnlyIdentity, badIdentity, ]) // Response { "0xboAddress": true, "0xV2OnlyAddress": false, "0xBadAddress": false, } ``` ```kotlin [Kotlin] // Request val boIdentity = Identity(ETHEREUM, '0xboAddress') val v2Identity = Identity(ETHEREUM, '0xV2OnlyAddress') val badIdentity = Identity(ETHEREUM, '0xBadAddress') val canMessage = client.canMessage(listOf(boIdentity, v2Identity, badIdentity)) // Response [ "0xboAddress": true, "0xV2OnlyAddress": false, "0xBadAddress": false, ] ``` ```swift [Swift] // Request let canMessage = try await client.canMessage([boIdentity, v2OnlyIdentity, badIdentity]) // Response [ "0xboAddress": true, "0xV2OnlyAddress": false, "0xBadAddress": false, ] ``` ::: ## Create a new group chat Once you have the verified identities, create a new group chat. The maximum group chat size is 250 members. :::tip If you want to provide faster and offline group creation, consider using [optimistic group chat creation](#optimistically-create-a-new-group-chat) instead. This approach enables instant group creation and message preparation before adding members and even when offline. ::: :::code-group ```js [Browser] const group = await client.conversations.newGroup( [bo.inboxId, caro.inboxId], createGroupOptions /* optional */ ); ``` ```js [Node] const group = await client.conversations.newGroup( [bo.inboxId, caro.inboxId], createGroupOptions /* optional */ ); ``` ```tsx [React Native] // New Group const group = await alix.conversations.newGroup([bo.inboxId, caro.inboxId]); // New Group with Metadata const group = await alix.conversations.newGroup([bo.inboxId, caro.inboxId], { name: 'The Group Name', imageUrl: 'www.groupImage.com', description: 'The description of the group', permissionLevel: 'admin_only', // 'all_members' | 'admin_only' }); ``` ```kotlin [Kotlin] // New Group val group = alix.conversations.newGroup(listOf(bo.inboxId, caro.inboxId)) // New Group with Metadata val group = alix.conversations.newGroup(listOf(bo.inboxId, caro.inboxId), permissionLevel = GroupPermissionPreconfiguration.ALL_MEMBERS, // ALL_MEMBERS | ADMIN_ONLY name = "The Group Name", imageUrl = "www.groupImage.com", description = "The description of the group", ) ``` ```swift [Swift] // New Group let group = try await alix.conversations.newGroup([bo.inboxId, caro.inboxId]) // New Group with Metadata let group = try await alix.conversations.newGroup([bo.inboxId, caro.inboxId], permissionLevel: .admin_only, // .all_members | .admin_only name: "The Group Name", imageUrl: "www.groupImage.com", description: "The description of the group", ) ``` ::: ## Optimistically create a new group chat Optimistic group creation enables instant group chat creation and message preparation before adding members and even when offline. This approach prioritizes user experience by allowing immediate interaction with the group chat, while handling the network synchronization in the background when members are added. Use this method to optimistically create a group chat, which enables a user to create a group chat now and add members later. The group chat can be created with any number of [standard options](#create-a-new-group-chat), or no options. The group chat is stored only in the local storage of the app installation used to create it. In other words, the group chat is visible only to the creator and in the app installation they used to create it. You can prepare messages for the optimistic group chat immediately using `prepareMessage()`. As with the group chat itself, these messages are stored locally only. When you want to add members, you use [`addMembers()`](#optimistically-create-a-new-group-chat) with a list of inbox IDs. Adding a member will automatically sync the group chat to the network. Once synced, the group chat becomes visible to the added members and across other app installations. After adding members, you must explicitly call `publishMessages()` to send any prepared messages to the network. To learn more about optimistically sending messages using `prepareMessage()` and `publishMessages()`, see [Optimistically send messages](/chat-apps/core-messaging/send-messages#optimistically-send-messages). :::code-group ```tsx [Browser] // create optimistic group (stays local) const optimisticGroup = await alixClient.conversations.newGroupOptimistic(); // send optimistic message (stays local) await optimisticGroup.sendOptimistic('gm'); // later, sync the group by adding members await optimisticGroup.addMembers([boClient.inboxId]); // or publishing messages await optimisticGroup.publishMessages(); ``` ```ts [Node] // create optimistic group (stays local) const optimisticGroup = client.conversations.newGroupOptimistic(); // send optimistic message (stays local) optimisticGroup.sendOptimistic('gm'); // later, sync the group by adding members await optimisticGroup.addMembers([boClient.inboxId]); // or publishing messages await optimisticGroup.publishMessages(); ``` ```tsx [React Native] const optimisticGroup = await boClient.conversations.newGroupOptimistic(); // Prepare a message (stays local) await optimisticGroup.prepareMessage('Hello group!'); // Later, add members and sync await optimisticGroup.addMembers([alixClient.inboxId]); // also syncs group to the network await optimisticGroup.publishMessages(); // Publish prepared messages ``` ```kotlin [Kotlin] // Create optimistic group (stays local) val optimisticGroup = boClient.conversations.newGroupOptimistic(groupName = "Testing") // Prepare a message (stays local) optimisticGroup.prepareMessage("Hello group!") // Later, add members and sync optimisticGroup.addMembers(listOf(alixClient.inboxId)) // also syncs group to the network optimisticGroup.publishMessages() // Publish prepared messages ``` ```swift [Swift] // Create optimistic group (stays local) let optimisticGroup = try await boClient.conversations.newGroupOptimistic(groupName: "Testing") // Prepare a message (stays local) try await optimisticGroup.prepareMessage("Hello group!") // Later, add members and sync try await optimisticGroup.addMembers([alixClient.inboxId]) // also syncs group to the network try await optimisticGroup.publishMessages() // Publish prepared messages ``` ::: ## Create a new DM Once you have the verified identity, get its inbox ID and create a new DM: :::code-group ```js [Browser] const group = await client.conversations.newDm(bo.inboxId); ``` ```js [Node] const group = await client.conversations.newDm(bo.inboxId); ``` ```tsx [React Native] const dm = await alix.conversations.findOrCreateDm(bo.inboxId); ``` ```kotlin [Kotlin] val dm = alix.conversations.findOrCreateDm(bo.inboxId) // calls the above function under the hood but returns a type conversation instead of a dm val conversation = client.conversations.newConversation(inboxId) ``` ```swift [Swift] let dm = try await alix.conversations.findOrCreateDm(with: bo.inboxId) // calls the above function under the hood but returns a type conversation instead of a dm let conversation = try await client.conversations.newConversation(inboxId) ``` ::: ## Conversation helper methods Use these helper methods to quickly locate and access specific conversations—whether by conversation ID, topic, group ID, or DM identity—returning the appropriate ConversationContainer, group, or DM object. :::code-group ```js [Browser] // get a conversation by its ID const conversationById = await client.conversations.getConversationById(conversationId); // get a message by its ID const messageById = await client.conversations.getMessageById(messageId); // get a 1:1 conversation by a peer's inbox ID const dmByInboxId = await client.conversations.getDmByInboxId(peerInboxId); ``` ```js [Node] // get a conversation by its ID const conversationById = await client.conversations.getConversationById(conversationId); // get a message by its ID const messageById = await client.conversations.getMessageById(messageId); // get a 1:1 conversation by a peer's inbox ID const dmByInboxId = await client.conversations.getDmByInboxId(peerInboxId); ``` ```tsx [React Native] // Returns a ConversationContainer await alix.conversations.findConversation(conversation.id); await alix.conversations.findConversationByTopic(conversation.topic); // Returns a Group await alix.conversations.findGroup(group.id); // Returns a DM await alix.conversations.findDmByIdentity(bo.identity); ``` ```kotlin [Kotlin] // Returns a ConversationContainer alix.conversations.findConversation(conversation.id) alix.conversations.findConversationByTopic(conversation.topic) // Returns a Group alix.conversations.findGroup(group.id) // Returns a DM alix.conversations.findDmbyInboxId(bo.inboxId); ``` ```swift [Swift] // Returns a ConversationContainer try alix.conversations.findConversation(conversation.id) try alix.conversations.findConversationByTopic(conversation.topic) // Returns a Group try alix.conversations.findGroup(group.id) // Returns a DM try alix.conversations. findDmbyInboxId(bo.inboxId) ``` ::: ## Conversation union type Serves as a unified structure for managing both group chats and DMs. It provides a consistent set of properties and methods to seamlessly handle various conversation types. - React Native: [Conversation.ts](https://github.com/xmtp/xmtp-react-native/blob/main/src/lib/Conversation.ts) ## Group class Represents a group chat conversation, providing methods to manage group-specific functionalities such as sending messages, synchronizing state, and handling group membership. - React Native: [Group.ts](https://github.com/xmtp/xmtp-react-native/blob/main/src/lib/Group.ts) ## Dm class Represents a DM conversation, providing methods to manage one-on-one communications, such as sending messages, synchronizing state, and handling message streams. - React Native: [Dm.ts](https://github.com/xmtp/xmtp-react-native/blob/main/src/lib/Dm.ts) ## chat-apps/core-messaging/disappearing-messages.mdx # Support disappearing messages Disappearing messages are messages that are intended to be visible to users for only a short period of time. After the message expiration time passes, the messages are removed from the UI and deleted from local storage so the messages are no longer accessible to conversation participants. ## App-level disappearing messages vs. network-level message expiration Disappearing message behavior is enforced by apps, meaning that apps are responsible for removing messages from their UIs and local storage based on conditions set at the conversation level. As a feature, disappearing messages doesn't delete messages from the XMTP network. Starting with XMTP mainnet, the network will enforce message expiration to delete messages from the network after a retention period currently targeted at 6 months. This message expiration is a general condition of the network and is not related to the disappearing messages feature. To learn more, see [Message expiry](https://community.xmtp.org/t/xip-49-decentralized-backend-for-mls-messages/856) in XIP-49: Decentralized backend for MLS messages. Disappearing messages can be understood as app-level privacy that helps avoid leaving an easily accessible record in a messaging UI, while XMTP mainnet message expiration is the mechanism by which messages are deleted from the network. ## Enable disappearing messages for a conversation Conversation participants using apps that support disappearing messages will have a UX that honors the message expiration conditions. Conversation participants using apps that don't support disappearing messages won't experience disappearing message behavior. Messages abide by the disappearing message settings for the conversation. When creating or updating a conversation, only group admins and DM participants can set disappearing message expiration conditions. This includes setting the following conditions expressed in nanoseconds (ns): - `disappearStartingAtNs`: Starting timestamp from which the message lifespan is calculated - `retentionDurationInNs`: Duration of time during which the message should remain visible to conversation participants For example: 1. Set `disappearStartingAtNs` to the current time, such as `1738620126404999936` (nanoseconds since the Unix epoch of January 1, 1970). 2. Set `retentionDurationInNs` to the message lifespan, such as 1800000000000000 (30 minutes). 3. Use `disappearStartingAtNs` and `retentionDurationInNs` to calculate the message expiration time of `1738620126404999936 + 1800000000000000 = 1740420126404999936`. To learn more see [conversation.rs](https://github.com/xmtp/libxmtp/blob/main/bindings_node/src/conversation.rs#L49). ## Set disappearing message settings on conversation create For example: :::code-group ```js [Browser] // DM await client.conversations.newDm(inboxId, { messageDisappearingSettings: { fromNs: 1738620126404999936n, inNs: 1800000000000000n, }, }); // Group await client.conversations.newGroup([inboxId], { messageDisappearingSettings: { fromNs: 1738620126404999936n, inNs: 1800000000000000n, }, }); ``` ```js [Node] // DM await client.conversations.newDm(inboxId, { messageDisappearingSettings: { fromNs: 1738620126404999936, inNs: 1800000000000000, }, }); // Group await client.conversations.newGroup([inboxId], { messageDisappearingSettings: { fromNs: 1738620126404999936, inNs: 1800000000000000, }, }); ``` ```tsx [React Native] // DM await client.conversations.newConversation( inboxId, { disappearingMessageSettings: DisappearingMessageSettings( disappearStartingAtNs: 1738620126404999936, retentionDurationInNs: 1800000000000000 ) } ) // Group await client.conversations.newGroup( [inboxId], { disappearingMessageSettings: DisappearingMessageSettings( disappearStartingAtNs: 1738620126404999936, retentionDurationInNs: 1800000000000000 ) } ) ``` ```kotlin [Kotlin] // DM client.conversations.newConversation( inboxId, disappearingMessageSettings = DisappearingMessageSettings( disappearStartingAtNs = 1738620126404999936, retentionDurationInNs = 1800000000000000 ) ) // Group client.conversations.newGroup( [inboxId], disappearingMessageSettings = DisappearingMessageSettings( disappearStartingAtNs = 1738620126404999936, retentionDurationInNs = 1800000000000000 ) ) ``` ```swift [Swift] // DM try await client.conversations.newConversation( with: inboxId, disappearingMessageSettings: DisappearingMessageSettings( disappearStartingAtNs: 1738620126404999936, retentionDurationInNs: 1800000000000000 ) ) // Group try await client.conversations.newGroup( with: [inboxId], disappearingMessageSettings: DisappearingMessageSettings( disappearStartingAtNs: 1738620126404999936, retentionDurationInNs: 1800000000000000 ) ) ``` ::: ## Update disappearing message settings for an existing conversation For example: :::code-group ```tsx [Browser] // Update disappearing message settings await conversation.updateMessageDisappearingSettings( 1738620126404999936n, 1800000000000000n ); // Clear disappearing message settings await conversation.removeMessageDisappearingSettings(); ``` ```tsx [Node] // Update disappearing message settings await conversation.updateMessageDisappearingSettings( 1738620126404999936, 1800000000000000 ); // Clear disappearing message settings await conversation.removeMessageDisappearingSettings(); ``` ```tsx [React Native] await conversation.updateDisappearingMessageSettings(updatedSettings); await conversation.clearDisappearingMessageSettings(); ``` ```kotlin [Kotlin] conversation.updateDisappearingMessageSettings(updatedSettings) conversation.clearDisappearingMessageSettings() ``` ```swift [Swift] try await conversation.updateDisappearingMessageSettings(updatedSettings) try await conversation.clearDisappearingMessageSettings() ``` ::: ## Get the disappearing message settings for a conversation For example: :::code-group ```tsx [Browser] // Get the disappearing message settings const settings = await conversation.messageDisappearingSettings(); // Check if disappearing messages are enabled const isEnabled = await conversation.isDisappearingMessagesEnabled(); ``` ```tsx [Node] // Get the disappearing message settings const settings = conversation.messageDisappearingSettings(); const isEnabled = conversation.isDisappearingMessagesEnabled(); ``` ```tsx [React Native] conversation.disappearingMessageSettings; conversation.isDisappearingMessagesEnabled(); ``` ```kotlin [Kotlin] conversation.disappearingMessageSettings conversation.isDisappearingMessagesEnabled ``` ```swift [Swift] conversation.disappearingMessageSettings try conversation.isDisappearingMessagesEnabled() ``` ::: ## Automatic deletion from local storage A background worker runs every one second to clean up expired disappearing messages. The worker automatically deletes expired messages from local storage. No additional action is required by integrators. To learn more about the background worker, see [disappearing_messages.rs](https://github.com/xmtp/libxmtp/blob/main/xmtp_mls/src/groups/disappearing_messages.rs#L68). ## Automatic removal from UI Expired messages don't require manual removal from the UI. If your app UI updates when the local storage changes, expired messages will disappear automatically when the background worker deletes them from local storage. ## Receive a disappearing message On the receiving side, your app doesn't need to check expiration conditions manually. Receive and process messages as usual, and the background worker handles message expiration cleanup. ## UX tips for disappearing messages To ensure that users understand which messages are disappearing messages and their behavior, consider implementing: - A distinct visual style: Style disappearing messages differently from regular messages (e.g., a different background color or icon) to indicate their temporary nature. - A clear indication of the message's temporary nature: Use a visual cue, such as a timestamp or a countdown, to inform users that the message will disappear after a certain period. ## chat-apps/core-messaging/group-metadata.mdx # Manage group chat metadata Group chats can have metadata, like names, descriptions, and images. Metadata can help users more easily identify their group chats. You can set group chat metadata when [creating a group chat](/chat-apps/core-messaging/create-conversations), and get and update metadata using these methods. ## Updatable group chat metadata The following group chat metadata can be updated: - `group_name`: The name of the group chat - `description`: A description of the group chat - `image_url`: A URL pointing to an image for the group chat - `disappearing_message_settings`: Settings for disappearing messages in the group chat. To learn more about disappearing messages, see [Support disappearing messages](/chat-apps/core-messaging/send-messages#support-disappearing-messages) ## Get a group chat name :::code-group ```js [Browser] const groupName = group.name; ``` ```js [Node] const groupName = group.name; ``` ```tsx [React Native] const groupName = await group.groupName(); ``` ```kotlin [Kotlin] group.name ``` ```swift [Swift] try group.groupname() ``` ::: ## Update a group chat name :::code-group ```js [Browser] await group.updateName('New Group Name'); ``` ```js [Node] await group.updateName('New Group Name'); ``` ```tsx [React Native] await group.updateName('New Group Name'); ``` ```kotlin [Kotlin] group.updateName("New Group Name") ``` ```swift [Swift] try await group.updateName(groupname: "New Group Name") ``` ::: ## Get a group chat description :::code-group ```js [Browser] const groupDescription = group.description; ``` ```js [Node] const groupDescription = group.description; ``` ```tsx [React Native] const groupDescription = await group.groupDescription(); ``` ```kotlin [Kotlin] group.description ``` ```swift [Swift] try group.groupDescription() ``` ::: ## Update a group chat description :::code-group ```js [Browser] await group.updateDescription('New Group Description'); ``` ```js [Node] await group.updateDescription('New Group Description'); ``` ```tsx [React Native] await group.updateDescription('New Group Description'); ``` ```kotlin [Kotlin] group.updateDescription("New Group Description") ``` ```swift [Swift] try await group.updateDescription(Description: "New Group Description") ``` ::: ## Get a group chat image URL :::code-group ```js [Browser] const groupImageUrl = group.imageUrl; ``` ```js [Node] const groupImageUrl = group.imageUrl; ``` ```tsx [React Native] const groupName = await group.imageUrl(); ``` ```kotlin [Kotlin] group.imageURL ``` ```swift [Swift] try group.imageUrl() ``` ::: ## Update a group chat image URL :::code-group ```js [Browser] await group.updateImageUrl('newurl.com'); ``` ```js [Node] await group.updateImageUrl('newurl.com'); ``` ```tsx [React Native] await group.updateImageUrl('ImageURL'); ``` ```kotlin [Kotlin] group.updateImageUrl("newurl.com") ``` ```swift [Swift] try await group.updateImageUrl(imageUrl: "newurl.com") ``` ::: ## chat-apps/core-messaging/group-permissions.mdx # Manage group permissions Robust group chat permissions are key to providing users with a friendly and safe group chat experience. ## Understand the group permissions system ### Member statuses Member statuses are the roles that can be assigned to each participant (inbox ID) in a group chat. These are the available member statuses: - Member - Everyone in a group chat is a member. A member can be granted admin or super admin status. If a member's admin or super admin status is removed, they are still a member of the group. - Admin - Super admin ### Options Use options to assign a role to a permission. These are the available options: - All members - Admin only - Includes super admins - Super admin only ### Permissions Permissions are the actions a group chat participant can be allowed to take. These are the available permissions: - Grant admin status to a member - Remove admin status from a member - Add a member to the group - Remove a member from the group - Update [group metadata](/chat-apps/core-messaging/group-metadata), such as group name, description, and image - Update group permissions on an item-by-item basis, such as calling `updateNamePermission` or `updateAddMemberPermission` . To learn more, see [Group.kt](https://github.com/xmtp/xmtp-android/blob/main/library/src/main/java/org/xmtp/android/library/Group.kt#L251-L313) in the xmtp-android SDK repo. The following permissions can be assigned by super admins only. This helps ensure that a “regular” admin cannot remove the super admin or otherwise destroy a group. - Grant super admin status to a member - Remove super admin status from a member - Update group permissions ## How the group permissions system works When a group is created, all groups have the same initial member "roles" set: - There is one super admin, and it is the group creator - There are no admins - Each user added to the group starts out as a member The super admin has all of the [available permissions](#permissions) and can use them to adjust the group's permissions and options. As the app developer, you can provide a UI that enables group participants to make further adjustments. For example, you can give the super admin the following permission options for group members when creating the group: - Add members - Update group metadata
UI screenshot showing group permission toggle options including Add members and Edit group info settings
You can use member statuses, options, and permissions to create a custom policy set. The following table represents the valid policy options for each of the permissions: | Permission | Allow all | Deny all | Admin only | Super admin only | | ------------------------ | --------- | -------- | ---------- | ---------------- | | Add member | ✅ | ✅ | ✅ | ✅ | | Remove member | ✅ | ✅ | ✅ | ✅ | | Add admin | ❌ | ✅ | ✅ | ✅ | | Remove admin | ❌ | ✅ | ✅ | ✅ | | Update group permissions | ❌ | ❌ | ❌ | ✅ | | Update group metadata | ✅ | ✅ | ✅ | ✅ | If you aren't opinionated and don't set any permissions and options, groups will default to using the delivered `All_Members` policy set, which applies the following permissions and options: - Add member - All members - Remove member - Admin only - Add admin - Super admin only - Remove admin - Super admin only - Update group permissions - Super admin only - Update group metadata - All members To learn more about the `All_Members` and `Admin_Only` policy sets, see [group_permissions.rs](https://github.com/xmtp/libxmtp/blob/85dd6d36f46db1ed74fe98273eea6871fea2e078/xmtp_mls/src/groups/group_permissions.rs#L1192-L1226) in the LibXMTP repo. ## Manage group chat admins ### Check if inbox ID is an admin :::code-group ```js [Browser] const isAdmin = group.isAdmin(inboxId); ``` ```js [Node] const isAdmin = group.isAdmin(inboxId); ``` ```tsx [React Native] // Assume group is an existing group chat object for client const isAdmin = await group.isAdmin(adminClient.inboxID); ``` ```kotlin [Kotlin] //Assume group is an existing group chat object for client val isInboxIDAdmin = group.isAdmin(inboxId) ``` ```swift [Swift] // Assume group is an existing group chat object for client try group.isAdmin(client.inboxID) ``` ::: ### Check if inbox ID is a super admin :::code-group ```js [Browser] const isSuperAdmin = group.isSuperAdmin(inboxId); ``` ```js [Node] const isSuperAdmin = group.isSuperAdmin(inboxId); ``` ```tsx [React Native] //Assume group is an existing group chat object for client const isSuperAdmin = await group.isSuperAdmin(client.inboxID); ``` ```kotlin [Kotlin] //Assume group is an existing group chat object for client val isInboxIDSuperAdmin = group.isSuperAdmin(inboxId) ``` ```swift [Swift] try group.isSuperAdmin(inboxid: inboxID) ``` ::: ### List admins :::code-group ```js [Browser] const admins = group.admins; ``` ```js [Node] const admins = group.admins; ``` ```tsx [React Native] await group.listAdmins(); ``` ```kotlin [Kotlin] // Returns a list of inboxIds of Admins group.listAdmins() ``` ```swift [Swift] try group.listAdmins() ``` ::: ### List super admins :::code-group ```js [Browser] const superAdmins = group.superAdmins; ``` ```js [Node] const superAdmins = group.superAdmins; ``` ```tsx [React Native] await group.listSuperAdmins(); ``` ```kotlin [Kotlin] // Returns a list of inboxIds of Super Admins group.listSuperAdmins() ``` ```swift [Swift] try group.listSuperAdmins() ``` ::: ### Add admin status to inbox ID :::code-group ```js [Browser] await group.addAdmin(inboxId); ``` ```js [Node] await group.addAdmin(inboxId); ``` ```tsx [React Native] await group.addAdmin(client.inboxID); ``` ```kotlin [Kotlin] group.addAdmin(inboxId) ``` ```swift [Swift] try await group.addAdmin(inboxid: inboxID) ``` ::: ### Add super admin status to inbox ID :::code-group ```js [Browser] await group.addSuperAdmin(inboxId); ``` ```js [Node] await group.addSuperAdmin(inboxId); ``` ```tsx [React Native] await group.addSuperAdmin(client.inboxID); ``` ```kotlin [Kotlin] group.addSuperAdmin(inboxId) ``` ```swift [Swift] try await group.addSuperAdmin(inboxid: inboxID) ``` ::: ### Remove admin status from inbox ID :::code-group ```js [Browser] await group.removeAdmin(inboxId); ``` ```js [Node] await group.removeAdmin(inboxId); ``` ```tsx [React Native] await group.removeAdmin(client.inboxID); ``` ```kotlin [Kotlin] group.removeAdmin(inboxId) ``` ```swift [Swift] try await group.removeAdmin(inboxid: inboxid) ``` ::: ### Remove super admin status from inbox ID :::code-group ```js [Browser] await group.removeSuperAdmin(inboxId); ``` ```js [Node] await group.removeSuperAdmin(inboxId); ``` ```tsx [React Native] await group.removeSuperAdmin(client.inboxId); ``` ```kotlin [Kotlin] group.removeSuperAdmin(inboxId) ``` ```swift [Swift] try await group.removeSuperAdmin(inboxid: inboxID) ``` ::: ## Manage group chat membership ### Add members by inbox ID The maximum group chat size is 250 members. :::code-group ```js [Browser] await group.addMembers([inboxId]); ``` ```js [Node] await group.addMembers([inboxId]); ``` ```tsx [React Native] await group.addMembers([inboxId]); ``` ```kotlin [Kotlin] group.addMembers(listOf(client.inboxId)) ``` ```swift [Swift] try await group.addMembers(inboxIds: [inboxId]) ``` ::: ### Remove member by inbox ID :::code-group ```js [Browser] await group.removeMembers([inboxId]); ``` ```js [Node] await group.removeMembers([inboxId]); ``` ```tsx [React Native] await group.removeMembers([inboxId]); ``` ```kotlin [Kotlin] group.removeMembers(listOf(inboxId)) ``` ```swift [Swift] try await group.removeMembers(inboxIds: [inboxId]) ``` ::: ### Get inbox IDs for members :::code-group ```js [Browser] const inboxId = await client.findInboxIdByIdentities([ bo.identity, caro.identity, ]); ``` ```js [Node] const inboxId = await client.getInboxIdByIdentities([ bo.identity, caro.identity, ]); ``` ```tsx [React Native] await group.memberInboxIds(); ``` ```kotlin [Kotlin] val members = group.members() val inboxIds = members.map { it.inboxId } OR val inboxId = client.inboxIdFromIdentity(peerIdentity) ``` ```swift [Swift] let members = try group.members.map(\.inboxId).sorted() OR try await client.inboxIdFromIdentity(identity: peerIdentity) ``` ::: ### Get identities for members :::code-group ```js [Browser] // sync group first await group.sync(); // get group members const members = await group.members(); // map inbox ID to account identity const inboxIdIdentityMap = new Map( members.map((member) => [member.inboxId, member.accountIdentity]) ); ``` ```js [Node] // sync group first await group.sync(); // get group members const members = group.members; // map inbox ID to account identity const inboxIdIdentityMap = new Map( members.map((member) => [member.inboxId, member.accountIdentity]) ); ``` ```tsx [React Native] const members = await group.members(); const identities = members.map((member) => member.identities); ``` ```kotlin [Kotlin] val members = group.members() val identities = members.map { it.identities } ``` ```swift [Swift] let peerMembers = try Conversation.group(group).peerInboxIds.sorted() ``` ::: ### Get the inbox ID that added the current member :::code-group ```js [Browser] const addedByInboxId = group.addedByInboxId; ``` ```js [Node] const addedByInboxId = group.addedByInboxId; ``` ```tsx [React Native] // this API is experimental and may change in the future const addedByInboxId = await group.addedByInboxId(); ``` ```kotlin [Kotlin] val addedByInboxId = group.addedByInboxId(); ``` ```swift [Swift] try await group.addedByInboxId(); ``` ::: ## chat-apps/core-messaging/manage-inboxes.mdx # Manage XMTP inboxes, identities, and installations With XMTP, a user can have one or more **inboxes** they use to access their messages. An inbox ID is a stable identifier for a user's messaging identity and is used as the destination for messages in direct message and group conversations. It is derived from the public key material in their key package. An inbox can have multiple **identities** associated with it. An identity has a kind, such as EOA or SCW, and a string, which in the case of an EOA or SCW, is an Ethereum address. However, this extensible inbox-based identity model means that support for additional kinds of identities, such as Passkey or Bitcoin, can be added in the future. All messages associated with these identities flow through the one inbox ID and are accessible in any XMTP app. The first time someone uses your app with an identity they've never used with any app built with XMTP, your app creates an inbox ID and **installation** associated with the identity. To do this, you [create a client](/chat-apps/core-messaging/create-a-client) for their identity. :::tip[Note for agent developers] The installation concepts described in this document apply to both inbox apps and agents. However, the context differs. **Inbox apps** typically have installations across different devices and apps (phone, laptop, different XMTP apps), while **agents** typically have installations across different deployment environments (local development, Railway, production servers). For more agent-specific guidance, see [Manage agent local database files and installations](/agents/build-agents/local-database). ::: The client creates an inbox ID and installation ID associated with the identity. By default, this identity is designated as the recovery identity. A recovery identity will always have the same inbox ID and cannot be reassigned to a different inbox ID.
Diagram showing creation of inbox ID and installation ID when a user first creates a client with their identity
When you make subsequent calls to create a client for the same identity and a local database is not present, the client uses the same inbox ID, but creates a new installation ID. An inbox ID can have up to 10 app installations before it needs to [revoke installations](#revoke-installations).
Diagram showing how creating a client for the same identity without a local database creates a new installation ID with the same inbox ID
You can enable a user to add multiple identities to their inbox. Added identities use the same inbox ID and the installation ID of the installation used to add the identity.
Diagram showing how adding multiple identities to an inbox uses the same inbox ID and installation ID
You can enable a user to remove an identity from their inbox. You cannot remove the recovery identity. ## Inbox update and installation limits ### 🎥 walkthrough: Installation limits and static revocation This video provides a walkthrough of the idea behind the XMTP installation limit (10) and how to use [static installation revocation](#revoke-installations-for-a-user-who-cant-log-in). After watching, feel free to continue reading for more details. ### Inbox update limits An inbox ID is limited to 256 inbox updates. Inbox updates include actions like: - Add a wallet - Remove a wallet - Add an installation - Revoke an installation :::danger[CRITICAL FOR PRODUCTION] Inbox updates are cumulative and **cannot be reversed**. Revoking an installation counts as an update but does not decrease the total count. Once you've performed 256 updates, you've reached the permanent limit for that inbox. ::: **What happens when you hit the limit:** If an inbox reaches its limit of 256 inbox updates, you may encounter an "inbox log is full" error. At this point, you must [rotate your inbox](#rotate-an-inbox-id) to continue using your identity with XMTP. Rotating the inbox permanently removes access to all existing conversations but allows you to continue using your identity with new XMTP conversations. **Why this limit exists:** All identity updates for an inbox must be fetched and validated by every other inbox that interacts with you on the XMTP network. In group conversations, this validation requirement multiplies. If multiple members have extensive identity update histories, it can significantly impact performance and message delivery speed. ### Installation limits XMTP enforces a 10-installation limit per inbox. When an inbox reaches 10 active installations, the user must revoke at least one installation before adding a new one. This limit serves two purposes: 1. Prevents excessive group sizes: Since all installations in an inbox are added as group members, limiting installations keeps group sizes manageable and maintains performance. 2. Protects against accidental exhaustion: The limit helps prevent users from accidentally reaching the 256 inbox update threshold. :::warning[Important] The only way to avoid consuming installation updates is to ensure your XMTP database is persisted and reloaded between app sessions and deployments. When you reuse an existing database, you maintain the same installation instead of creating a new one each time. ::: ### Best Practices **For testing and development:** If you're developing or testing and frequently creating new installations, you can quickly exhaust your inbox update limit. To avoid this during development: 1. Use persistent storage: Always configure your deployments to preserve the database between restarts 2. Use a local node: Run XMTP locally so you can reset it when hitting limits 3. Use different wallets for testing: Create separate test wallets rather than repeatedly creating installations with the same wallet 4. Monitor your usage: Regularly check your inbox state to track how many updates you've used Have feedback about the inbox update and installation limits? We'd love to hear it. Post to the [XMTP Community Forums](https://community.xmtp.org/). ### Rotate an inbox ID When you create an inbox ID, a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce) is used to create the ID. If an inbox reaches its inbox updates limit, you can increment the nonce to create a new inbox ID associated with the same wallet address. :::danger[CRITICAL FOR PRODUCTION] Rotating an inbox creates a completely new inbox ID. All conversations and message history from the old inbox will be permanently lost. Other users will need to start new conversations with your new inbox ID. ::: ## Help users manage installations By default, your app stores the following information about installations: - `id` (random byte array) - `createdAt` (date) To help users make informed decisions when managing and revoking their installations, you should aim to store additional useful information about installations, such as: - Last date logged in - Device make and model Installation IDs that don't have this additional information can be treated as unknown installations, which can also provide helpful signals to users. ## Revoke installations ### 🎥 walkthrough: Revoking installations This video provides a walkthrough of revoking installations, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. ### Revoke a specific installation You can revoke one or more specific installations by ID, as long as it isn't the currently accessed installation. An inbox ID can have up to 10 app installations before it needs to revoke an installation. - If an inbox ID has 10 installations and the user wants to add another installation, use this function to enable them to revoke an installation before they can add the new one. - If an inbox ID was created before this installation limit was implemented, the inbox ID might have more than 10 installations. If this is the case, the user will receive an error when they try to add another installation. Use this function to enable them to revoke the required number of installations before they can add the new one. For example, if an inbox ID has 20 installations, the user will need to revoke 11 installations before they can add the new one. If preferred, you can use the [revoke all other installations](#revoke-all-other-installations) function to revoke all installations other than the currently accessed installation. :::code-group ```tsx [Browser] await client.revokeInstallations([installationId1, installationId2]); ``` ```tsx [Node] await client.revokeInstallations([installationId1, installationId2]); ``` ```jsx [React Native] await client.revokeInstallations(signingKey, [installationIds]); ``` ```kotlin [Kotlin] client.revokeInstallations(signingKey, listOf(installationIds)) ``` ```swift [Swift] try await client.revokeInstallations(signingKey, [installationIds]) ``` ::: ### Revoke all other installations You can revoke all installations other than the currently accessed installation. For example, consider a user using this current installation:
Diagram showing a user's current installation among multiple installations for their inbox
When the user revokes all other installations, the action removes their identity's access to all installations other than the current installation:
Diagram showing the result after revoking all other installations, leaving only the current installation active
An inbox ID can have up to 10 app installations before it needs to revoke an installation. If preferred, you can use the [revoke a specific installation](#revoke-a-specific-installation) function to revoke one or more specific installations by ID. :::code-group ```tsx [Browser] await client.revokeAllOtherInstallations(); ``` ```tsx [Node] await client.revokeAllOtherInstallations(); ``` ```jsx [React Native] await client.revokeAllOtherInstallations(signingKey); ``` ```kotlin [Kotlin] client.revokeAllOtherInstallations(signingKey) ``` ```swift [Swift] try await client.revokeAllOtherInstallations(signingKey) ``` ::: ### Revoke installations for a user who can't log in For a video walkthrough of this feature, see [🎥 walkthrough: Installation limits and static revocation](#-walkthrough-installation-limits-and-static-revocation). Static installation revocation enables users to revoke installations without needing to log in or have access to their installations. This feature is especially useful in the following scenarios: - A user has reached the 10 installation limit and can't log in to revoke installations to make room for a new installation. - A user logs out of an app installation and chooses the option to delete their local database. Choosing this option will cause them to permanently lose access to the installation. For this reason, you should revoke the installation. In both scenarios, static revocation enables logged out users to revoke installations using only their recovery address signer. Here is how static installation revocation works behind the scenes: 1. Determines which installation IDs to revoke 2. Generates a signature request for the revocation 3. Uses the recovery address signer to authorize the revocation 4. Submits the signed request to revoke the specified installations :::code-group ```tsx [Browser] const inboxStates = await Client.inboxStateFromInboxIds([inboxId], "production"); const toRevokeInstallationBytes = inboxStates[0].installations.map((i) => i.bytes); await Client.revokeInstallations( signer, inboxId, toRevokeInstallationBytes, "production", // optional, defaults to "dev" enableLogging: true, // optional, defaults to false ); ``` ```tsx [Node] const inboxStates = await Client.inboxStateFromInboxIds( [inboxId], 'production' ); const toRevokeInstallationBytes = inboxStates[0].installations.map( (i) => i.bytes ); await Client.revokeInstallations( signer, inboxId, toRevokeInstallationBytes, 'production' // optional, defaults to "dev" ); ``` ```jsx [React Native] const states = await Client.inboxStatesForInboxIds('production', [inboxId]) const toRevokeIds = states[0].installations.map((i) => i.id) await Client.revokeInstallations( 'production', recoveryWallet, inboxId, toRevokeIds as InstallationId[] ) ``` ```kotlin [Kotlin] val states = Client.inboxStatesForInboxIds( listOf(inboxId), api) val toRevokeIds = states.first().installations.map { it.id } Client.revokeInstallations( api, recoveryWallet, inboxId, toRevokeIds ) ``` ```swift [Swift] let states = try await Client.inboxStatesForInboxIds(inboxIds: [inboxId], api) let toRevokeIds = states.first.installations.map { $0.id } try await Client.revokeInstallations( api: api, signingKey: recoveryWallet, inboxId: inboxId, installationIds: toRevokeIds ) ``` ::: ## View the inbox state Find an `inboxId` for an identity: :::code-group ```tsx [Browser] const inboxState = await client.preferences.inboxState(); ``` ```tsx [Node] const inboxState = await client.preferences.inboxState(); ``` ```jsx [React Native] const inboxId = await client.inboxIdFromIdentity(identity); ``` ```kotlin [Kotlin] val inboxId = client.inboxIdFromIdentity(identity) ``` ```swift [Swift] let inboxId = try await client.inboxIdFromIdentity(identity: identity) ``` ::: View the state of any inbox to see the identities, installations, and other information associated with the `inboxId`. ### Sample request :::code-group ```tsx [Browser] // the second argument is optional and refreshes the state from the network. const states = await client.preferences.inboxStateFromInboxIds( [inboxId, inboxId], true ); ``` ```tsx [Node] // the second argument is optional and refreshes the state from the network. const states = await client.preferences.inboxStateFromInboxIds( [inboxId, inboxId], true ); ``` ```jsx [React Native] const state = await client.inboxState(true); const states = await client.inboxStates(true, [inboxId, inboxId]); ``` ```kotlin [Kotlin] val state = client.inboxState(true) val states = client.inboxStatesForInboxIds(true, listOf(inboxID, inboxID)) ``` ```swift [Swift] let state = try await client.inboxState(refreshFromNetwork: true) let states = try await client.inboxStatesForInboxIds( refreshFromNetwork: true, inboxIds: [inboxID, inboxID] ) ``` ::: #### Sample response ```json InboxState { "recoveryIdentity": "string", "identities": [ { "kind": "ETHEREUM", "identifier": "string", "relyingPartner": "string" }, { "kind": "PASSKEY", // not yet supported; provided as an example only. "identifier": "string", "relyingPartner": "string" } ], "installations": ["string"], "inboxId": "string" } ``` ## Add an identity to an inbox :::warning[Warning] This function is delicate and should be used with caution. Adding an identity to an inbox ID B when it's already associated with an inbox ID A will cause the identity to lose access to inbox ID A. ::: :::code-group ```tsx [Browser] await client.unsafe_addAccount(signer, true); ``` ```tsx [Node] await client.unsafe_addAccount(signer, true); ``` ```jsx [React Native] await client.addAccount(identityToAdd); ``` ```kotlin [Kotlin] client.addAccount(identityToAdd) ``` ```swift [Swift] try await client.addAccount(newAccount: identityToAdd) ``` ::: ## Remove an identity from an inbox :::tip[Note] A recovery identity cannot be removed. For example, if an inbox has only one associated identity, that identity serves as the recovery identity and cannot be removed. ::: :::code-group ```tsx [Browser] await client.removeAccount(identifier); ``` ```tsx [Node] await client.removeAccount(identifier); ``` ```jsx [React Native] await client.removeAccount(recoveryIdentity, identityToRemove); ``` ```kotlin [Kotlin] client.removeAccount(recoveryIdentity, identityToRemove) ``` ```swift [Swift] try await client.removeAccount(recoveryIdentity: recoveryIdentity, identityToRemove: identityToRemove) ``` ::: ## Select the identity to display When an inbox has multiple associated identities, the `identities` array is ordered by the `client_timestamp_ns` field, which sorts identities based on when they were added to the inbox, placing the earliest added identity first. This provides consistent display ordering and helps establish norms for which identity should be used for UI display purposes. For UI display purposes, you can use the following logic to select the most appropriate identity: 1. If there is only one identity in the `identities` array, use that as the display identity. 2. If there are two identities, use the one that does not match the `recoveryIdentity`. The first non-recovery identifier in the array is typically the preferred identity for display. 3. If there are more than two identities, the first non-recovery identity in the ordered array is typically the preferred identity for display. ## FAQ ### What happens when a user removes an identity? Consider an inbox with three associated identities:
Diagram showing an inbox with three associated identities (wallet addresses)
If the user removes an identity from the inbox, the identity no longer has access to the inbox it was removed from.
Diagram showing the state after removing one identity from the inbox, leaving two remaining identities
The identity can no longer be added to or used to access conversations in that inbox. If someone sends a message to the identity, the message is not associated with the original inbox. If the user logs in to a new installation with the identity, this will create a new inbox ID.
Diagram showing how a removed identity creates a new inbox ID when logging into a new installation
### How is the recovery identity used? The recovery identity and its signer can be used to sign transactions that remove identities and revoke installations. For example, Alix can give Bo access to their inbox so Bo can see their groups and conversations and respond for Alix. If Alix decides they no longer want Bo have access to their inbox, Alix can use their recovery identity signer to remove Bo. However, while Bo has access to Alix's inbox, Bo cannot remove Alix from their own inbox because Bo does not have access to Alix's recovery identity signer. ### If a user created two inboxes using two identities, is there a way to combine the inboxes? If a user logs in with an identity with address 0x62EE...309c and creates inbox 1 and then logs in with an identity with address 0xd0e4...DCe8 and creates inbox 2; there is no way to combine inbox 1 and 2.
Diagram showing two separate inboxes created by logging in with two different identities
You can add an identity with address 0xd0e4...DCe8 to inbox 1, but both identities with addresses 0x62EE...309c and 0xd0e4...DCe8 would then have access to inbox 1 only. The identity with address 0xd0e4...DCe8 would no longer be able to access inbox 2.
Diagram showing how adding an identity to inbox 1 removes its access to inbox 2
To help users avoid this state, ensure that your UX surfaces their ability to add multiple identities to a single inbox. ### What happens if I remove an identity from an inbox ID and then initiate a client with the private key of the removed identity? **Does the client create a new inbox ID or does it match it with the original inbox ID the identity was removed from?** The identity used to initiate a client should be matched to its original inbox ID. You do have the ability to rotate inbox IDs if a user reaches the limit of 256 identity actions (adding, removing, or revoking identities or installations). Hopefully, users won't reach this limit, but if they do, inbox IDs have a nonce and can be created an infinite number of times. However, anytime a new inbox ID is created for an identity, the conversations and messages in any existing inbox ID associated with the identity are lost. ### I have multiple identities associated with one inbox ID. If I log in with any one of these identities, does it access that inbox ID, or does it create a new inbox ID? The identity accesses that inbox ID and does not create a new inbox ID. For example, let's say that you create a client with an identity with address 0x62EE...309c. Inbox ID 1 is generated from that identity.
Simple diagram showing inbox ID 1 with one identity
If you then add an identity with address 0xd0e4...DCe8 to inbox ID 1, the identity is also associated with inbox ID 1. If you then log into a new app installation with the identity with address 0xd0e4...DCe8, it accesses inbox ID 1 and does not create a new inbox ID. Once the identity with address 0xd0e4...DCe8 has been associated with inbox ID 1, it can then be used to log into inbox ID 1 using a new app installation.
Diagram showing inbox ID 1 with two identities after adding a second identity
The inverse is also true. Let's say an identity with address 0xd0e4...DCe8 was previously used to create and log into inbox ID 2.
Diagram showing two separate inboxes, each with their own identity, before identity migration
If the identity is then added as an associated identity to inbox ID 1, the identity will no longer be able to log into inbox ID 2.
Diagram showing the result after moving an identity from inbox 2 to inbox 1, leaving inbox 2 empty
To enable the user of the identity with address 0xd0e4...DCe8 to log into inbox ID 2 again, you can use the recovery identity for inbox ID 2 to add a different identity to inbox ID 2 and have the user use that identity access it. If you are interested in providing this functionality in your app and want some guidance, post to the [XMTP Community Forums](https://community.xmtp.org). ## chat-apps/core-messaging/rate-limits.mdx # Observe rate limits XMTP enforces separate rate limits for read and write operations per client per rolling 5-minute window: - **Read operations**: 20,000 requests per 5-minute window - **Write operations**: 3,000 messages published per 5-minute window When you reach either rate limit, your API calls will be rejected with a 429 (Too Many Requests) error. **Read operations** include actions like: - Fetching conversations - Retrieving messages - Getting inbox state - Listing installations **Write operations** include actions like: - Sending chat messages - Adding and removing wallets - Adding and revoking installations - Adding and removing group members - Updating group metadata ## chat-apps/core-messaging/send-messages.mdx # Send messages Once you have the group chat or DM conversation, you can send messages in the conversation. :::code-group ```tsx [Browser] // For a DM conversation await dm.send('Hello world'); // OR for a group chat await group.send('Hello everyone'); ``` ```tsx [Node] // For a DM conversation await dm.send('Hello world'); // OR for a group chat await group.send('Hello everyone'); ``` ```tsx [React Native] // For a DM conversation const dm = await client.conversations.findOrCreateDm(recipientInboxId); await dm.send('Hello world'); // OR for a group chat const group = await client.conversations.newGroup([ recipientInboxId1, recipientInboxId2, ]); await group.send('Hello everyone'); ``` ```kotlin [Kotlin] // For a DM conversation val dm = client.conversations.findOrCreateDm(recipientInboxId) dm.send(text = "Hello world") // OR for a group chat val group = client.conversations.newGroup(listOf(recipientInboxId1, recipientInboxId2)) group.send(text = "Hello everyone") ``` ```swift [Swift] // For a DM conversation let dm = try await client.conversations.findOrCreateDm(with: recipientInboxId) try await dm.send(content: "Hello world") // OR for a group chat let group = try await client.conversations.newGroup([recipientInboxId1, recipientInboxId2]) try await group.send(content: "Hello everyone") ``` ::: ## Optimistically send messages When a user sends a message with XMTP, they might experience a slight delay between sending the message and seeing their sent message display in their app UI. Typically, the slight delay is caused by the app needing to wait for the XMTP network to finish processing the message before the app can display the message in its UI. Messaging without optimistic sending: ![Messaging without optimistic sending. Note the slight delay after clicking Send.](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/without-opt-sending.gif) Note the slight delay after clicking **Send**. Implement optimistic sending to be able to immediately display the sent message in the sender's UI while processing the message in the background. This provides the user with immediate feedback and enables them to continue messaging without waiting for their previous message to finish processing. Messaging with optimistic sending: ![Messaging with optimistic sending. The message displays immediately for the sender, with a checkmark indicator displaying once the message has been successfully sent.](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/with-opt-sending.gif) The message displays immediately for the sender, with a checkmark indicator displaying once the message has been successfully sent. ### How it works There are two steps to optimistically send a message: 1. Send the message to the local database so you can display it immediately in the sender's UI. 2. Publish the message to the XMTP network so it can be delivered to the recipient. ### 1. Optimistically send a message locally Send the message to the local database. This ensures that the message will be there when you query for messages and can immediately display the message in the sender's UI. :::code-group ```tsx [Browser] // Optimistically send the message to the local database conversation.sendOptimistic('Hello world'); // For custom content types, specify the content type const customContent = { foo: 'bar' }; const contentType = { authorityId: 'example', typeId: 'test', versionMajor: 1, versionMinor: 0, }; conversation.sendOptimistic(customContent, contentType); ``` ```tsx [Node] // Optimistically send the message to the local database conversation.sendOptimistic('Hello world'); // For custom content types, specify the content type const customContent = { foo: 'bar' }; const contentType = { authorityId: 'example', typeId: 'test', versionMajor: 1, versionMinor: 0, }; conversation.sendOptimistic(customContent, contentType); ``` ```tsx [React Native] // Optimistically send the message to the local database await conversation.prepareMessage('Hello world'); // For custom content types, specify the content type const customContent = { foo: 'bar' }; const contentType = new ContentTypeId({ authorityId: 'example', typeId: 'test', versionMajor: 1, versionMinor: 0, }); await conversation.prepareMessage(customContent, contentType); ``` ```kotlin [Kotlin] // Optimistically send the message to the local database conversation.prepareMessage("Hello world") // For custom content types, specify the content type val customContent = mapOf("foo" to "bar") val contentType = ContentTypeId( authorityId = "example", typeId = "test", versionMajor = 1, versionMinor = 0 ) conversation.prepareMessage(customContent, contentType) ``` ```swift [Swift] // Optimistically send the message to the local database try await conversation.prepareMessage("Hello world") // For custom content types, specify the content type let customContent = ["foo": "bar"] let contentType = ContentTypeId( authorityId: "example", typeId: "test", versionMajor: 1, versionMinor: 0 ) try await conversation.prepareMessage(customContent, contentType: contentType) ``` ::: ### 2. Publish an optimistically sent message to the network After optimistically sending a message, use `publishMessages` to publish the message to the XMTP network so it can be delivered to recipients. :::code-group ```tsx [Browser] // Publish all pending optimistically sent messages to the network // Call this only after using sendOptimistic to send a message locally async function sendMessageWithOptimisticUI(conversation, messageText) { try { // Add message to UI immediately conversation.sendOptimistic(messageText); // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { console.error('Failed to send message:', error); return false; } } ``` ```tsx [Node] // Publish all pending optimistically sent messages to the network // Call this only after using sendOptimistic to send a message locally async function sendMessageWithOptimisticUI(conversation, messageText) { try { // Add message to UI immediately conversation.sendOptimistic(messageText); // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { console.error('Failed to send message:', error); return false; } } ``` ```tsx [React Native] // Publish all pending optimistically sent messages to the network // Call this only after using prepareMessage to send a message locally async function sendMessageWithOptimisticUI( conversation: Conversation, messageText: string ): Promise { try { // Add message to UI immediately await conversation.prepareMessage(messageText); // Actually send the message to the network await conversation.publishMessages(); return true; } catch (error) { console.error('Failed to send message:', error); return false; } } ``` ```kotlin [Kotlin] // Publish all pending optimistically sent messages to the network // Call this only after using prepareMessage to send a message locally suspend fun sendMessageWithOptimisticUI(conversation: Conversation, messageText: String): Boolean { return try { // Add message to UI immediately conversation.prepareMessage(messageText) // Actually send the message to the network conversation.publishMessages() true } catch (error: Exception) { Log.e("XMTP", "Failed to send message: ${error.message}", error) false } } ``` ```swift [Swift] // Publish all pending optimistically sent messages to the network // Call this only after using prepareMessage to send a message locally func sendMessageWithOptimisticUI(conversation: Conversation, messageText: String) async throws -> Bool { do { // Add message to UI immediately try await conversation.prepareMessage(messageText) // Actually send the message to the network try await conversation.publishMessages() return true } catch { print("Failed to send message: \(error)") return false } } ``` ::: ### Key UX considerations for optimistically sent messages - After optimistically sending a message, show the user an indicator that the message is still being processed. After successfully sending the message, show the user a success indicator. - An optimistically sent message initially has an `unpublished` status. Once published to the network, it has a `published` status. You can use this status to determine which indicator to display in the UI. - If an optimistically sent message fails to send it will have a `failed` status. In this case, be sure to give the user an option to retry sending the message or cancel sending. Use a try/catch block to intercept errors and allow the user to retry or cancel. ## Handle unsupported content types As more [custom](/chat-apps/content-types/custom) and [standards-track](/chat-apps/content-types/content-types#standards-track-content-types) content types are introduced into the XMTP ecosystem, your app may encounter content types it does not support. This situation, if not handled properly, could lead to app crashes. Each message is accompanied by a `fallback` property, which offers a descriptive string representing the content type's expected value. It's important to note that fallbacks are immutable and are predefined in the content type specification. In instances where `fallback` is `undefined`, such as read receipts, it indicates that the content is not intended to be rendered. If you're venturing into creating custom content types, you're provided with the flexibility to specify a custom fallback string. For a deeper dive into this, see [Build custom content types](/chat-apps/content-types/custom). :::code-group ```js [Browser] const codec = client.codecFor(content.contentType); if (!codec) { /*Not supported content type*/ if (message.fallback !== undefined) { return message.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```js [Node] const codec = client.codecFor(content.contentType); if (!codec) { /*Not supported content type*/ if (message.fallback !== undefined) { return message.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```jsx [React Native] //contentTypeID has the following structure `${contentType.authorityId}/${contentType.typeId}:${contentType.versionMajor}.${contentType.versionMinor}`; const isRegistered = message.contentTypeID in client.codecRegistry; if (!isRegistered) { // Not supported content type if (message?.fallback != null) { return message?.fallback; } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```kotlin [Kotlin] val codec = client.codecRegistry.find(options?.contentType) if (!codec) { /*Not supported content type*/ if (message.fallback != null) { return message.fallback } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ```swift [Swift] let codec = client.codecRegistry.find(for: contentType) if (!codec) { /*Not supported content type*/ if (message.fallback != null) { return message.fallback } // Handle other types like ReadReceipts which are not meant to be displayed } ``` ::: ## chat-apps/core-messaging/support-group-invite-links.mdx # Support group chat invite links This document provides suggested paths you might take to provide XMTP group chat invite links in your app. These are just proposals and the actual implementation direction and details are specific to your app and your goals. ## Possible user experience flows 1. A group member with permission to add group members [creates an invite link](#create-a-group-invite-link). 2. An non-member clicks the invite link to [request to join the group](#generate-an-invite-landing-page). 3. The non-member is granted access to the group in one of the following ways: - [Automatic join via silent push notification to the link creator](#automatic-join-via-silent-push-notification-to-link-creator) - [Automatic join via silent push notification to all group members](#automatic-join-via-silent-push-notification-to-all-group-members) - [Manual join via push notification to the link creator](#manual-join-via-push-notification-to-link-creator) 4. [Add the invitee to the group](#add-the-member-to-the-group) 5. [Check and manage the invite status](#check-the-invite-status) ### Sequence diagrams These diagrams help illustrate the sequence of interactions between users and participating systems for the suggested options for granting group access to an invitee. #### Option 1: Automatic join via silent push notification to the link creator Sequence diagram for Automatic join via silent push notification to the link creator #### Option 2: Automatic join via silent push notification to all group members Sequence diagram for Automatic join via silent push notification to all group members #### Option 3: Manual join via push notification to the link creator Sequence diagram for Manual join via push notification to the link creator ## Create a group invite link You can provide a UI that enables a group member with permission to add members to create an invite link. Create the group invite by making a `POST` to a `/groupInvite` endpoint with any metadata your app wants to show on an invite landing page that will be displayed when an invitee clicks the invite link. ### `POST /groupInvite` #### Example request ```json { "groupName": "XMTP Builders", "groupImage": "https://..." // Inviter can be inferred from the request auth token. // Backend doesn't need the actual group ID, since the client can keep track in their DB } ``` #### Example response The backend returns a new invite link with a unique URL. Save the `linkUrl` to the client's local database (alongside the XMTP `group_id`) so the link can be surfaced in the UI for copy/paste later and used in push notification handlers. ```json { "id": "abcdefg", "linkUrl": "https://converse.xyz/invite/abcdefg" } ``` ## Generate an invite landing page When a user clicks the invite link, the link can display a group invite landing page at `app.xyz`, for example. To get the information to generate the invite landing page, you can make a `GET` to a `/groupInvite/:id` endpoint. For example, the invite landing page can display the group name, the invite link creator's profile, any other group metadata the app wants to show, and a Join button. When a user clicks the Join button on the invite landing page: - If the user doesn't have the app installed, the link can take them to an app store where they can be prompted to install the app. Once installed, the user can be returned to the invite landing page, where they can click Join again. - If the user does have the app installed, the link can open the app. The app can display the conversation list view with the new group grayed out to indicate a pending status. ### `GET /groupInvite/:id` #### Example request ```html GET /groupInvite/abcdefg ``` #### Example response ```json { id: "abcdefg", linkUrl: "https://app.xyz/invite/abcdefg", groupName: "XMTP Builders", groupImage: "https://...", createdBy: } ``` ### `POST /groupJoinRequest` Can be used by the invitee to request to join the group. #### Example request ```json { "inviteId": "abcdefg" // User info implied from auth token } ``` #### Example response ```json { "id": "hijklmn", // The id of the join request "status": "pending" } ``` ## Handle the request to join the group When the invitee clicks the invite link to request to join the group, consider granting them access to the group in one of the following ways. ### Automatic join via silent push notification to link creator As soon as the link creator is detected as being online, they can receive a silent push notification that automatically adds the invitee to the group. This can make the join appear automatic. #### Example request ```json { "inviteId": "abcdefg", "joinRequestId": "hijklmn", "recipientId": "user123", "groupName": "XMTP Builders", "type": "auto_join_creator" } ``` #### Example response ```json { "success": true, "status": "processing", "message": "Silent push notification sent to link creator" } ``` ### Automatic join via silent push notification to all group members If the group permits all members to add members, consider sending a silent background push notification to all group members. The first member detected as being online can receive a silent push notification that automatically adds the invitee to the group. This can make the join appear automatic. This setup can take the dependency off the sole link creator being online and spreads it across the group, which can allow the group join to happen faster. #### Example request ```json { "inviteId": "abcdefg", "joinRequestId": "hijklmn", "recipientId": "user123", "groupName": "XMTP Builders", "type": "auto_join_members", "groupMembers": ["member1", "member2", "member3"] } ``` #### Example response ```json { "success": true, "status": "processing", "message": "Silent push notifications sent to all group members", "notificationsSent": 3 } ``` ### Manual join via push notification to link creator Consider having the link creator receive a push notification about a specific invitee requesting to join the group using their invite link. Enable the link creator to approve or reject the request. For example, you can send a push notification to the invite link creator saying, "User X has requested to join Group Y through your invite link." Give the link creator a way to approve or reject the request. #### Example request ```json { "inviteId": "abcdefg", "joinRequestId": "hijklmn", "recipientId": "user123", "groupName": "XMTP Builders", "type": "manual_approval", "requesterName": "Alix A", "requesterAvatar": "https://example.com/avatar.jpg" } ``` #### Example response ```json { "success": true, "status": "pending_approval", "message": "Push notification sent to link creator for manual approval" } ``` ### Handle push notification delivery failure To handle cases where the push notification fails to deliver (maybe the user is offline for a while), XMTP can provide an API that enables clients to check for any pending joins. Want XMTP to build this API? [Open an issue](https://github.com/xmtp/libxmtp/issues) in the LibXMTP repo to request it. #### Example request ```json { "userId": "user123", "lastCheckTimestamp": "2024-03-20T10:00:00Z" } ``` #### Example response ```json { "pendingJoins": [ { "inviteId": "abcdefg", "joinRequestId": "hijklmn", "groupName": "XMTP Builders", "status": "pending", "requestedAt": "2024-03-20T09:30:00Z" } ], "lastCheckTimestamp": "2024-03-20T10:00:00Z" } ``` ## Add the member to the group If the invite link creator approves the request, in the background, you can call LibXMTP to load the group and add the member. ### Example request ```json { "inviteId": "abcdefg" // User info implied from auth token } ``` ### Example response ```json { id: "hijklmn" // id of the join request status: 'pending' } ``` ## Check the invite status ### `/groups/joinFromInvite` You can poll a `/groups/joinFromInvite` endpoint to check the invite status and know when the invite link creator has marked it as approved or rejected. ### `GET /groupJoinRequest/:id` You can provide this endpoint as a way for an invitee to check the status of their request to join a group. #### Example request ```html GET /groupJoinRequest/hijklmn ``` #### Example response ```json { id: "hijklmn", status: "approved"// Possible statuses ("approved", "rejected", "pending") reason: null // Allow the system to pass a reason back to the client } ``` ## Mark the invite request as complete Once the invite link creator has approved or rejected the request, you can make a `PUT` to a `/groupJoinRequest/:id` endpoint to mark the request as completed. ### `PUT /groupJoinRequest/:id` #### Example request You can have the invite link creator mark an invite as approved or rejected. For basic invite links, approval can happen as soon as the push notification has been received. ```json { "status": "approved" } ``` #### Example response ```json { "id": "hijklmn", "status": "approved", "reason": null // Allow the system to pass a reason back to the client } ``` # Section: protocol ## protocol/cursors.mdx # Cursors with XMTP This document explains the concept of **cursors** as they relate to message synchronization on the XMTP network. Cursors are a fundamental part of how XMTP clients efficiently fetch new messages and maintain state, particularly with the `sync()` family of functions. While cursors are managed automatically by the XMTP SDKs, understanding how they work is crucial for debugging and for grasping the underlying mechanics of message synchronization. ## What is a cursor? A cursor is a pointer or a marker that an XMTP client maintains for each topic it subscribes to (both [group message topics](/protocol/topics#group-message-topic) and [welcome message topics](/protocol/topics#welcome-message-topic)). This cursor is stored locally and is specific to each app installation. Think of it as a bookmark in the chronological log of messages and events for a given topic. Its purpose is to remember the exact point up to which an installation has successfully synchronized its data. ## How cursors work with `sync()` The primary role of a cursor becomes evident when you use the `sync()` functions (`conversation.sync()`, `conversations.sync()`, and `conversations.syncAll()`). 1. **Initial sync**: The first time an app installation calls `sync()` for a specific conversation, it fetches all available messages and events from the network for that conversation's topic. 2. **Cursor placement**: Once the sync is complete, the SDK places a cursor at the end of that batch of fetched messages. 3. **Subsequent syncs**: On the next `sync()` call for that same conversation, the client sends its current cursor position to the network. The network then returns only the messages and events that have occurred _after_ that cursor. 4. **Cursor advancement**: After the new messages are successfully fetched, the SDK advances the cursor to the new latest point. This process ensures that each `sync()` call only retrieves what's new, making synchronization efficient by avoiding the re-downloading of messages the client already has. ### How Cursors Enable Efficient Sync The XMTP SDKs use cursors to make message synchronization highly efficient. The design principle is to fetch new data from the network with `sync()` while providing access to historical data from a local database. - **`sync()` fetches new data from the network:** The `sync()` functions are designed specifically to retrieve new messages and events from the network. To do this efficiently, the SDK advances the cursor to the position of the last synchronized item. On subsequent `sync()` calls, the client provides this cursor, and the network returns only what has arrived since. This forward-only cursor movement is an intentional design choice that prevents re-downloading data the client already has. - **Access old messages from the local database:** Once `sync()` fetches messages from the network, they are stored in a local database managed by the SDK. You can query this database at any time to retrieve historical messages without making a network request. This provides fast, local access to the full message history available to the installation. - **History on new devices is handled by history sync:** The behavior of cursors should not be confused with loading message history on a new device. A new app installation lacks the encryption keys to decrypt old messages. Even if it could fetch them from the network, they would be unreadable. [History sync](/chat-apps/list-stream-sync/history-sync) is the dedicated process for securely transferring message history and the necessary encryption keys to a new installation. - **Streaming does not affect the cursor:** Receiving messages via a real-time `stream()` does not move the cursor. Streaming provides instant message delivery but doesn't guarantee order or completeness if the client is briefly offline. `sync()` serves as the mechanism to ensure the local state is complete and correctly ordered, and only then is the cursor advanced. ## Cursors for different sync functions Each `sync()` function corresponds to a different type of cursor: - `conversation.sync()`: This operates on the **group message topic** for a single conversation. It moves the cursor for that specific conversation, fetching new messages or group updates (like name changes). - `conversations.sync()`: This operates on the **welcome message topic**. It moves the cursor for welcome messages, fetching any new conversations the user has been invited to. It does _not_ fetch the contents of those new conversations. - `conversations.syncAll()`: This is the most comprehensive sync. It effectively performs the actions of the other two syncs for all of the user's conversations. It moves the cursors for the welcome topic _and_ for every individual group message topic, ensuring the client has fetched all new conversations and all new messages in existing conversations. For example, here is a sequence diagram illustrating how cursors operate with `conversation.sync()`: ![Sequence diagram showing cursor flow during conversation.sync() operation, illustrating how cursors track message positions and enable incremental synchronization](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/cursor-flow.png) By understanding cursors, you can better reason about the behavior of your app's synchronization logic and the data being transferred from the XMTP network. ## protocol/envelope-types.mdx # Envelope types with XMTP This document covers **envelope types** that clients can publish to XMTP APIs. These are the top-level message structures that can be sent and received through the XMTP network. This information is primarily useful for: - Developers contributing to the XMTP protocol itself - Understanding XMTP internals and debugging - Reading XMTP Improvement Proposals (XIPs) The envelope types described here are handled automatically by XMTP SDKs and rarely need direct interaction from app developers. For **app development**, you'll typically work with [content types](/chat-apps/content-types/content-types) instead. Content types define how your app's messages are structured and encoded (text, attachments, reactions, etc.) and are what you'll use in your day-to-day development. ## Overview XMTP supports several envelope types that clients can publish to the network: - **Group message envelopes**: Contain MLS protocol messages (application messages and commits) - **Welcome message envelopes**: Bootstrap new members into existing groups - **Key package envelopes**: Register cryptographic credentials for user installations - **Identity update envelopes**: Verify and authenticate user identities These envelope types work together to enable secure group communication with [forward secrecy and post-compromise security](/protocol/security). **Note**: In the MLS context, "group" refers to any collection of clients that share cryptographic state, which includes both direct message (1:1) or group chat conversations. ## Group message envelope A group message envelope contains an MLS protocol message that can be either an application message or a commit message. This is the primary envelope type used for day-to-day communication. ### Application messages Application messages represent the actual content that users send to each other, including: - Text messages - Attachments - Reactions - Read receipts - On-chain transaction references - Custom content types Application messages are: - Encrypted using the group's shared secrets - Authenticated with sender signatures - Encoded using XMTP content types - Sent on the `groupMessage` topic ### Commit messages Commit messages update the group's cryptographic state, membership, and permissions. While technically a single message type, commit messages serve many different purposes. Understanding commit messages is especially helpful when debugging and understanding your app's user experience. For example: - Different commit types help explain why certain messages appear in logs - Some commits happen invisibly, while others are tied to user actions #### User-initiated commits - **Add member commits**: When a user explicitly adds someone to a group - **Remove member commits**: When a user removes someone from a group - **Update metadata commits**: When a user changes group name, description, or permissions - **Update permissions commits**: When a user modifies group permission settings #### System-initiated commits - **Key update commits**: Automatically generated when a new member joins before sending their first message - **Missing member commits**: Triggered when the system detects someone is missing from the group - **Scheduled commits**: Periodic commits for security maintenance #### MLS protocol commits - **Update path commits**: Generated by the MLS protocol for key rotation and security - **External sender commits**: For handling external participants For more information about MLS commits, see [RFC 9420 Section 12.4](https://www.rfc-editor.org/rfc/rfc9420.html#section-12.4). ## Welcome message envelope A welcome message envelope bootstraps a new member into an existing group. The welcome is dependent on the newcomer's [key package](#key-package-envelope) and provides the new member with the current state of the group after application of a [commit message](#commit-messages). A welcome message contains: - Group context information - Encrypted group secrets - Tree structure for the group - Confirmation tags for epoch verification If decryption fails due to an outdated or missing key package, the SDK automatically fetches the latest package and retries. ## Key package envelope A key package envelope registers cryptographic credentials for a user installation. XMTP SDKs create and upload fresh key packages behind the scenes when an installation is initialized or rotated. Think of a key package as a calling card for an installation that says: here's how to encrypt to me, here's how long this card is valid, and it's signed so you can trust it. A key package contains: - Public key for encrypting welcome messages - Signature key for authentication - Capabilities (supported protocol version, cipher suites, lifetime, etc.) - Credential for identity verification - Content of the leaf node representing this client Group members cache key packages to authenticate future handshakes and welcome material, enabling asynchronous addition of clients to groups. When an app inspects a group member, the SDK provides a `PublicIdentity` object containing the decoded fields, allowing apps to display identity information or check if the key package has expired. ## Identity update envelope An identity update envelope verifies and authenticates a user identity across the network. It is signed by the user's identity and verifiable by peers. Identity update envelopes enable group members to rotate their signature or HPKE keys while preserving group continuity and authenticity. They handle: - Linking an installation to an XMTP inbox - Key rotation and revocation - Linking multiple devices (though [history sync](/chat-apps/list-stream-sync/history-sync) is used to synchronize data between those devices) Identity update messages are stored permanently to ensure continuity of trust and identity verification. ## protocol/epochs.mdx # Epochs with XMTP With XMTP, each [commit](/protocol/envelope-types#commit-messages) starts a new epoch, which represents the current cryptographic state of a group chat. Epochs are a core concept from [Messaging Layer Security](https://messaginglayersecurity.rocks/) (MLS), which XMTP implements for secure group messaging. Epochs work according to these requirements: - Sequential numbering: Epochs are strictly ordered and increase by one with each commit (epoch 1, epoch 2, etc.). - New keys: Each epoch introduces a fresh encryption key, and keys from previous epochs are discarded (with the exception of a configurable number of recent past epochs that may be retained for special cases). - Decryption requirement: To read messages or commits in a given epoch, a member must have the correct epoch key. - Fork risk: If members diverge on which epoch they're in (e.g., due to missed or out-of-order commits), they won't be able to decrypt each other's messages, causing a fork. [Intents](/protocol/intents) help ensure two types of ordering: - **Epoch ordering**: Commits must be published in the correct epoch (one greater than the last applied epoch) to be processed by clients - **Consistent ordering**: All clients must receive published commits in the same order to prevent forks, regardless of epoch validity Intents achieve epoch ordering by enabling retries, while relying on the server's guarantee of consistent ordering across all clients. ## Handle concurrent commits When multiple commits arrive at nearly the same time, XMTP uses a "first-to-arrive wins" approach. For example, if commits 2 and 3 both attempt to advance from epoch 1, whichever commit arrived first becomes the accepted next epoch, and the other commit is rejected. For example, if commit 2 arrived first, it will be built on epoch 2 and commit 3 will be rejected. However, [intents](/protocol/intents) provide a mechanism for the rejected commit 3 to be retried. The intent can be reprocessed against the new epoch 2 state, allowing the operation to succeed in the updated context rather than being permanently lost. ## protocol/identity.mdx # Identity model with XMTP XMTP's identity model includes an inbox ID and its associated identities and installations. With an inbox ID at its core, instead of a specific wallet address or other identity value, the model is designed for extensibility. A user can associate any number of identity types to their inbox ID and use them to send and receive messages. This gives the user the freedom to add and remove identity types as they naturally change over time, while maintaining the same stable inbox destination for their messages. ![Diagram showing a core inbox ID associated with multiple identities, with access to multiple apps](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/multi-id.png) The identity model also allows XMTP to support any identity type, as long as it can produce a verifiable cryptographic signature. Currently supported identity types include Ethereum EOAs, Ethereum smart contract wallets, and passkeys. ## Inbox ID An **inbox ID** is a user's stable destination for their messages. Their inbox ID remains constant even as they add or remove [identities](#identity) and [installations](#installation). The inbox ID is derived from the hash of the first associated wallet address and a nonce and acts as an opaque identifier that apps use for messaging. ## Identity An **identity** is an addressable account that can be associated with an inbox ID. Each identity has a type (like EOA, smart contract wallet, or passkey) and an identifier (like an Ethereum address). - Multiple identities can be linked to a single inbox ID - The first identity becomes the **recovery identity** with special privileges - All messages sent to any associated identity are delivered to the same inbox - Any identity that can produce a verifiable cryptographic signature can be supported by XMTP ## Installation An **installation** represents a specific app installation that can access an inbox. Each installation has its own cryptographic keys for signing messages and participating in conversations. - Generated automatically when `Client.create()` is called for the first time with an identity that hasn't been used with XMTP before - Multiple installations can access the same inbox (up to 10) - Installations can be revoked by the recovery identity ## Relationships **One inbox ID** → **multiple identities**: Users can receive messages as any of their identities, all flowing to the same inbox ```text Inbox ID (stable destination for messages) ├── Identity 1 (recovery identity, first identity added to an inbox) ├── Identity 2 (EOA wallet) ├── Identity 3 (SCW wallet) └── Any identity that can produce a verifiable cryptographic signature ``` **One identity** → **multiple installations**: Users can access their messages from different apps on the same or different devices ```text Each identity can authenticate new installations: ├── Installation A (phone app) ├── Installation B (web app) ├── Installation C (desktop app) └── Up to 10 installations ``` ## Identity actions To learn how to build agents with identity actions, see [Manage agent local database files and installations](/agents/build-agents/local-database). To learn how to build chat apps with identity actions, see [Manage XMTP inboxes, identities, and installations](/chat-apps/core-messaging/manage-inboxes). ## protocol/intents.mdx # Intents with XMTP Intents provide an internal state machine, or "bookkeeping" mechanism, for reliably applying changes to XMTP group chat states, even when the process encounters retries, crashes, race conditions, or ordering issues. Developers building apps and agents with XMTP don't need to work with intents directly, but understanding them provides insight into how the protocol maintains integrity behind the scenes. ## Intent actions An intent represents an action that intends to change the state of a group chat via a [commit](/protocol/envelope-types#commit-messages), along with enough information to retry the action if it fails. Each commit rotates the group's encryption state into a new [epoch](/protocol/epochs) and must be applied in epoch order. If a client processes a commit that is in an incorrect epoch, it will simply discard the commit. Intents provide a structured way to track the multi-step process of publishing commits, handling retries, and recovering from interruptions. This ensures that every commit is eventually published in the correct epoch. Examples of intent actions include: - Add member: Add a participant to a group - Remove member: Remove a participant from a group - Send message: Deliver an application message to the group - Change metadata: Rename a group, for example ## Intent states Each intent progresses through a series of states as it is processed: - **To publish**: Intent has been created and queued, but not yet sent - **Published**: Commit has been sent to the network - **Error**: Intent failed with a permanent, non-retryable error (for example, a member without adequate permission tries to add a member, or the member was removed from the group). These intents will not be retried. - Note: Temporary, retryable failures (such as network issues or app restarts) keep the intent in the **To publish** state for retry on the next sync. - **Committed**: Commit has been accepted into the group's state, and dependent operations can now be performed. For example, after adding group members, welcome messages can be sent with the new encryption state. - **Processed**: Intent is fully complete and all related operations have finished By tracking intent states, XMTP ensures that if an app crashes before a commit has been accepted, for example, the commit process can resume later from the stored state without losing intent information. ## Example intent flow ![Flow diagram showing the progression of an intent through different states: Pending → Committed → Processed, illustrating how XMTP tracks and manages group operations](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/intents-flow.png) ## protocol/overview.mdx # XMTP protocol overview XMTP is a decentralized messaging protocol that enables secure, end-to-end encrypted communication between any identities that can produce a verifiable cryptographic signature. XMTP implements [Messaging Layer Security](https://messaginglayersecurity.rocks/) (MLS), which is designed to operate within the context of a messaging service. As the messaging service, XMTP needs to provide two services to facilitate messaging using MLS: - An [authentication service](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-authentication-service) - A [delivery service](https://messaginglayersecurity.rocks/mls-architecture/draft-ietf-mls-architecture.html#name-delivery-service) This section covers the elements of XMTP that provide these services. :::info[Who should read these docs] This protocol documentation is designed for: - Protocol contributors working on XMTP's core implementation - Security researchers auditing XMTP's cryptographic design - Anyone curious about the technical details behind XMTP's messaging For most developers, the [Build chat apps](/chat-apps/intro/get-started) and [Build agents](/agents/get-started/build-an-agent) sections provide the practical guidance needed to build with XMTP. ::: ## Encryption The encryption elements are mainly defined by MLS, with some additions by XMTP. To learn more, see: - [Security](/protocol/security) XMTP and MLS prioritize security, privacy, and message integrity through advanced cryptographic techniques, delivering end-to-end encryption for both 1:1 and group conversations - [Epochs](/protocol/epochs) Represent the cryptographic state of a group at any point in time. Each group operation (like adding members) creates a new epoch with fresh encryption keys - [Envelope types](/protocol/envelope-types) Messages are packaged as envelope types that contain the actual message data plus metadata for routing and processing. ## Identity The identity elements are mainly defined by XMTP. To learn more, see: - [Inboxes, identities, and installations](/protocol/identity) The identity model includes an inbox ID and its associated identities and installations. - [Wallet signatures](/protocol/signatures) Authenticate users using verifiable cryptographic signatures. ## Delivery The delivery elements are mainly defined by XMTP. To learn more, see: - [Topics](/protocol/topics) Messages are routed through topics, which are unique addresses that identify conversation channels. - [Cursors](/protocol/cursors) Enable efficient message synchronization by tracking where each client left off when fetching new messages. - [Intents](/protocol/intents) Provide reliable groupstate management through an internal bookkeeping system that handles retries, crashes, and race conditions when applying group changes. ## Protocol evolution XMTP evolves through **[XMTP Improvement Proposals](/protocol/xips)** (XIPs), which are design documents that propose new features and improvements. This governance process ensures systematic and decentralized protocol development. ## Additional resources For a broader vision of XMTP's approach to core concepts, see the following topics on [xmtp.org](https://xmtp.org): - [Security](https://xmtp.org/vision/concepts/encryption) - [Identity](https://xmtp.org/vision/concepts/identity) - [Consent](https://xmtp.org/vision/concepts/consent) - [Decentralizing XMTP](https://xmtp.org/vision/concepts/decentralizing-xmtp) ## protocol/security.mdx # Messaging security properties with XMTP XMTP delivers end-to-end encrypted 1:1 and group chat using the following resources: - Advanced cryptographic techniques - Secure key management practices - MLS ([Messaging Layer Security](https://www.rfc-editor.org/rfc/rfc9420.html)) Specifically, XMTP messaging provides the comprehensive security properties covered in the following sections. In these sections, **group** refers to the MLS concept of a group, which includes both 1:1 and group conversations. 🎥 **walkthrough: XMTP and MLS** This video provides a walkthrough of XMTP's implementation of MLS. To dive deeper into how XMTP implements MLS, see the [XMTP MLS protocol specification](https://github.com/xmtp/libxmtp/tree/main/xmtp_mls). ## A deep dive into messaging security properties ### Message confidentiality Ensures that the contents of messages in transit can't be read without the corresponding encryption keys. Message confidentiality is achieved through symmetric encryption, ensuring that only intended recipients can read the message content. [AEAD](#cryptographic-tools-in-use) (Authenticated Encryption with Associated Data) is used to encrypt the message content, providing robust protection against unauthorized access. ## Forward secrecy Ensures that even if current session keys are compromised, past messages remain secure. MLS achieves this by using the ratcheting mechanism, where the keys used to encrypt application messages are ratcheted forward every time a message is sent. When the old key is deleted, old messages can't be decrypted, even if the newer keys are known. This property is supported by using ephemeral keys during the key encapsulation process. ## Post-compromise security Ensures that future messages remain secure even if current encryption keys are compromised. XMTP uses regular key rotation achieved through a commit mechanism with a specific update path in MLS, meaning a new group secret is encrypted to all other members. This essentially resets the key and an attacker with the old state can't derive the new secret, as long as the private key from the leaf node in the ratchet tree construction hasn't been compromised. This ensures forward secrecy and protection against future compromises. ## Message authentication Validates the identity of the participants in the conversation, preventing impersonation. XMTP uses digital signatures to strongly guarantee message authenticity. These signatures ensure that each message is cryptographically signed by the sender, verifying the sender's identity without revealing it to unauthorized parties. This prevents attackers from impersonating conversation participants. ## Message integrity Ensures that messages can't be tampered with during transit and that messages are genuine and unaltered. XMTP achieves this through the use of MLS. The combination of digital signatures and [AEAD](#cryptographic-tools-in-use) enables XMTP to detect changes to message content. ## Quantum resistance Protects against future quantum computer attacks through post-quantum cryptography. XMTP implements quantum-resistant encryption to protect against "Harvest Now, Decrypt Later" (HNDL) attacks, where adversaries store encrypted messages until quantum computers become powerful enough to break current encryption. XMTP uses a hybrid approach that combines post-quantum algorithms with conventional cryptography, ensuring protection against future quantum threats without compromising current security. The quantum resistance is implemented by securing Welcome messages (the entry point for all conversations) with post-quantum key encapsulation. Since Welcome messages contain the group secrets needed to decrypt all messages in a conversation, protecting them with quantum-resistant encryption ensures the entire conversation remains secure against quantum attacks. Once inside a group, all messages maintain the same size and performance characteristics as before. To learn more about how XMTP achieves quantum resistance, see [XMTP and the Future of Privacy in a Quantum World](https://community.xmtp.org/t/xmtp-and-the-future-of-privacy-in-a-quantum-world/1079). ## User anonymity Ensures that outsiders can't deduce the participants of a group, users who have interacted with each other, or the sender or recipient of individual messages. User anonymity is achieved through a combination of the following functions: - MLS Welcome messages encrypt the sender metadata and group ID, protecting the social graph. - XMTP adds a layer of encryption to MLS Welcome messages using [HPKE](#cryptographic-tools-in-use) (Hybrid Public Key Encryption). This prevents multiple recipients of the same Welcome message from being correlated to the same group. - XMTP uses MLS [PrivateMessage](https://www.rfc-editor.org/rfc/rfc9420.html#name-confidentiality-of-sender-d) framing to hide the sender and content of group messages. - XMTP's backend doesn't authenticate reads or writes and only implements per-IP rate limits. Aside from Welcome messages, all payloads for a given group are stored under a single group ID, and any client may anonymously query or write to any group ID. Only legitimate members possess the correct encryption keys for a given group. It's technically possible for XMTP network node operators to analyze query patterns per IP address. However, clients may choose to obfuscate this information using proxying/onion routing. XMTP currently hides the sender of Welcome messages (used to add users to a group) but doesn't hide the Welcome message recipients. This makes it possible to determine how many groups a user was invited to but not whether the user did anything about the invitations. ## Cryptographic tools in use XMTP messaging uses the ciphersuite _MLS_128_HPKEX25519_CHACHA20POLY1305_SHA256_Ed25519_. Here is a summary of individual cryptographic tools used to collectively ensure that XMTP messaging is secure, authenticated, and tamper-proof: - [HPKE](https://www.rfc-editor.org/rfc/rfc9180.html) Used to encrypt Welcome messages, protect the identities of group invitees, and maintain the confidentiality of group membership. We use the ciphersuite HPKEX25519. - [AEAD](https://developers.google.com/tink/aead) Used to ensure both confidentiality and integrity of messages. In particular, we use the ciphersuite CHACHA20POLY1305. - [SHA3_256 and SHA2_256](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) XMTP uses two cryptographic hash functions to ensure data integrity and provide strong cryptographic binding. SHA3_256 is used in the multi-wallet identity structure. SHA2_256 is used in MLS. The ciphersuite is SHA256. - [Ed25519](https://ed25519.cr.yp.to/ed25519-20110926.pdf) Used for digital signatures to provide secure, high-performance signing and verification of messages. The ciphersuite is Ed25519. - [XWING KEM](https://www.ietf.org/archive/id/draft-connolly-cfrg-xwing-kem-02.html) Used for quantum-resistant key encapsulation in Welcome messages. XWING is a hybrid post-quantum KEM that combines conventional cryptography with [ML-KEM](https://csrc.nist.gov/pubs/fips/203/final) (the NIST-standardized post-quantum component), providing protection against future quantum computer attacks while maintaining current security standards. ## FAQ about messaging security 1. **Can XMTP read user messages?** No, messages are encrypted end-to-end. Only participants in a conversation have the keys to decrypt the messages in it. Your app cannot decrypt messages either. 2. **How does XMTP's encryption compare to Signal or WhatsApp?** XMTP provides the same security properties (forward secrecy and post-compromise security) as Signal and WhatsApp, using the newer, more efficient MLS protocol. 3. **Can others see who users are messaging with?** No. Message recipients are encrypted, so even network nodes cannot see who is messaging whom. Nodes can only see timing and size of encrypted messages. 4. **What happens if a user loses access to their wallet?** They'll need to start new conversations from their new wallet. Messages sent to their old wallet address can't be decrypted without access to that wallet. 5. **Are group messages as secure as direct messages?** Yes, MLS provides the same security properties for both group and direct messages. In fact, MLS is particularly efficient for group messaging. 6. **What if a user suspects their wallet is compromised?** Due to forward secrecy, even if someone gains access to their wallet, they can't read their past messages. They should start using a new wallet immediately - this ensures attackers won't be able to read future messages either. 7. **How does encryption work across different XMTP apps?** All XMTP apps use the same MLS protocol, ensuring consistent encryption across the ecosystem regardless of which app users choose. ## protocol/signatures.mdx --- description: 'Learn about wallet signature types when using XMTP' --- # Wallet signatures with XMTP Learn about the types of wallet address signatures you might be prompted to provide when using apps built with XMTP. These signatures are always made with a specific wallet address controlled by your wallet. ## First-time app installation use The first time you use an installation of an app built with XMTP, a **Sign this message?** window displays to request that you sign an **XMTP : Authenticate to inbox** message. For example: ```text XMTP : Authenticate to inbox Inbox ID: ${INBOX_ID} Current time: ${YYYY-MM-DD HH:MM:SS UTC} ``` More specifically, the message will request that you sign: - A **Grant messaging access to app** message to grant the app installation access to messaging owned by your signing wallet address. For example: ```text - Grant messaging access to app (ID: ${hex(INSTALLATION_PUBLIC_KEY)}) ``` - A **Create inbox** message to create an XMTP inbox owned by your signing address, but only if you have never used an app installation built with XMTP v3 before. For example: ```text - Create inbox (Owner: ${INITIAL_ADDRESS}) ``` Sign the **XMTP : Authenticate to inbox** message with your wallet address to consent to the message requests. MetaMask wallet browser extension Sign this message? window showing an XMTP: Authenticate to inbox message ## Sign to add another address to your inbox You can add another wallet address to your inbox at any time. For example, you might have started using an app with one wallet address and now want to use the app with an additional wallet address. If you decide to add another wallet address to your inbox, a **Sign this message?** window displays to request that you sign an **XMTP : Authenticate to inbox** message. Specifically, the message requests that you sign a **Link address to inbox** message. For example: ```text - Link address to inbox (Address: ${ASSOCIATED_ADDRESS}) ``` Sign with the wallet address you want to add to grant it access to the inbox. You can now use your inbox to exchange messages using the wallet address you just added. ## Sign to remove address from your inbox You can remove a wallet address from your inbox at any time. If you decide to remove a wallet address from your inbox, a **Sign this message?** window displays to request that you sign an **XMTP : Authenticate to inbox** message. Specifically, the message requests that you sign an **Unlink address from inbox** message. For example: ```text - Unlink address from inbox (Address: ${ASSOCIATED_ADDRESS}) ``` Sign with the wallet address you want to remove to unlink it from your inbox. You can no longer access your inbox using the wallet address you removed. ## Sign to change inbox recovery address The first time you used an app installation built with XMTP v3, the wallet address you used to create an inbox is automatically set as the inbox recovery address. You can change the recovery address to a different wallet address at any time. If you decide to change the recovery address, a **Sign this message?** window displays to request that you sign an **XMTP : Authenticate to inbox** message. Specifically, the message requests that you sign a **Change inbox recovery address** message. For example: ```text - Change inbox recovery address (Address: ${NEW_RECOVERY_ADDRESS}) ``` Sign with the wallet address you want to set as the recovery address to change the recovery address. ## Sign to consent to receive broadcast messages When you click a **Subscribe** button built with XMTP's consent standards, you're prompted to sign an **XMTP : Grant inbox consent to sender** message. For example, here's the MetaMask **Signature request** window that displays when clicking the **Subscribe** button on this [example subscription page](https://subscribe-broadcast.vercel.app/subscribe/button) connected to the XMTP `dev` network. You typically see **Subscribe** buttons like this on a web page or in a dapp. ![MetaMask wallet browser extension Signature request window showing an "XMTP: Grant inbox consent to sender" message](https://raw.githubusercontent.com/xmtp/docs-xmtp-org/main/docs/pages/img/consent-proof-sign.png) When you click **Sign**, you're consenting to receive broadcast messages from the publisher at your connected wallet address. You can see the publisher's sending address in the **Signature request** window. When you provide consent, you're adding the publisher's address to your personal XMTP allowed contacts list. This enables messages from the publisher to be displayed in your main inbox instead of being treated as a message from an unknown sender and placed in a secondary view. To learn about XMTP's consent standards, see [Understand how user consent preferences support spam-free inboxes](/chat-apps/user-consent/user-consent). ## protocol/topics.mdx # Topics with XMTP This document describes the concept of **topics** on the XMTP network. Topics are used to address and route [envelopes](/protocol/envelope-types), forming the backbone of the XMTP pub/sub messaging system. While XMTP SDKs manage topic subscriptions automatically, understanding them can be helpful for protocol-level development, debugging, and building services like push notification servers. ## Topic naming convention XMTP topics follow a standardized format that indicates the protocol version, message type, and a unique identifier. The general structure is: `/xmtp/mls/1/{topic-type}-{identifier}/proto` - `/xmtp/mls/1/`: The protocol namespace, indicating XMTP with MLS, version 1. - `{topic-type}`: A single letter representing the purpose of the topic (for example, `g` for group, `w` for welcome). - `{identifier}`: A unique ID for the specific conversation or installation. - `/proto`: The payload serialization format. ## Core topics in XMTP XMTP uses two primary topic types for delivering messages. ### Group message topic The group message topic is used to send and receive messages within a specific conversation (both 1:1 DMs and group chats). Each conversation has its own unique topic. - **Format**: `/xmtp/mls/1/g-$conversationId/proto` - **Envelope**: [Group message envelope](/protocol/envelope-types#group-message-envelope) - **Purpose**: Carries all ongoing communication for a conversation, including [application messages](/protocol/envelope-types#application-messages) (text, reactions, etc.) and [commit messages](/protocol/envelope-types#commit-messages) that modify the group state. - **Usage**: When an app wants to receive messages for a conversation, it subscribes to this topic. The `conversation.topic` property in the SDKs provides this value. > \*\*Note on [DM stitching](/chat-apps/push-notifs/understand-push-notifs#understand-dm-stitching-and-push-notifications): For direct messages, multiple underlying conversations might be "stitched" together in the UI. For push notifications to be reliable, an app must subscribe to the group message topic for each of these underlying conversations. ### Welcome message topic The welcome message topic is used to deliver a `Welcome` message to a new member of a group. This message bootstraps the new member, providing them with the group's state so they can participate. - **Format**: `/xmtp/mls/1/w-$installationId/proto` - **Envelope**: [Welcome message envelope](/protocol/envelope-types#welcome-message-envelope) - **Purpose**: To notify a specific app installation that it has been added to a new conversation. - **Usage**: A push notification server subscribes to an installation's welcome topic to be notified when that installation is invited to a new group chat or DM. The SDKs provide this via methods like `client.welcomeTopic()`. ## How other envelopes are handled Not all envelope types are broadcast on persistent pub/sub topics. - **[Key package envelopes](/protocol/envelope-types#key-package-envelope)**: These are not sent over a topic. Instead, they are published to a network-level store where they can be retrieved by other clients who need to start a conversation or add a new member to a group. - **[Identity update envelopes](/protocol/envelope-types#identity-update-envelope)**: These are also not sent over a topic. They are stored permanently on the XMTP network to ensure the continuity and verifiability of a user's identity across their devices. ## protocol/xips.mdx # XMTP Improvement Proposals (XIPs) An XIP is a design document that proposes a new feature or improvement for XMTP or its processes or environment. XIPs intend to be the primary mechanisms for: - Proposing new features - Collecting community technical input on an issue - Documenting the design decisions that have gone into XMTP For these reasons, XIPs are a great way to learn about XMTP's newest features and participate in shaping the evolution of the protocol. To review the latest XIPs, see [Improvement Proposals](https://community.xmtp.org/c/xips/xip-drafts/53). To learn more about XIPs and how to participate in the process, including authoring an XIP of your own, see [XIP purpose, process, and guidelines](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-0-purpose-process.md). # Section: network ## network/network-nodes.mdx # XMTP testnet nodes For real-time statuses of nodes in the XMTP testnet, see [XMTP Node Status](https://status.testnet.xmtp-partners.xyz/). The following table lists the nodes currently registered to power the XMTP testnet. | | Node operator | Node address | | --- | --------------------------- | --------------------------------------------- | | 1 | Artifact Capital | xmtp.artifact.systems:443 | | 2 | Crystal One | xmtp.node-op.com:443 | | 3 | Emerald Onion | xmtp.disobey.net:443 | | 4 | Encapsulate | lb.validator.xmtp.testnet.encapsulate.xyz:443 | | 5 | Ethereum Name Service (ENS) | grpc.ens-xmtp.com:443 | | 6 | Laminated Labs | xmtp.validators.laminatedlabs.net:443 | | 7 | Next.id | xmtp.nextnext.id:443 | | 8 | Nodle | xmtpd.nodleprotocol.io:443 | | 9 | Ephemera | grpc.testnet.xmtp.network:443 | | 10 | Ephemera | grpc2.testnet.xmtp.network:443 | Here is a map of node locations:
A purple pin indicates that the node is operated by a non-profit organization. ## network/run-a-node.mdx # Run an XMTP network node A [testnet of the decentralized XMTP network](/network/network-nodes) was launched in early December 2024. The XMTP testnet is powered by registered node operators running [xmtpd](https://github.com/xmtp/xmtpd), XMTP daemon software. xmtpd will also power mainnet. To learn more about the decentralized network, see [Decentralizing XMTP](https://xmtp.org/vision/concepts/decentralizing-xmtp). ## Interested in becoming a registered XMTP node operator? 1. Review [XIP-54: XMTP network node operator qualification criteria](https://community.xmtp.org/t/xip-54-xmtp-network-node-operator-qualification-criteria/868). 2. [Submit an application](https://docs.google.com/forms/d/e/1FAIpQLScBpJ0i962xBPpZZeI6q7-UMo5Bc2JkhvHR_v2rliLDoBkzXQ/viewform?usp=sharing) to become a registered node operator. ## Already a registered node operator? To get started, see the [xmtpd-infrastructure](https://github.com/xmtp/xmtpd-infrastructure) repository, which provides infrastructure-as-code examples and tooling to help node operators deploy and manage xmtpd nodes. ## FAQ about the XMTP network To follow and provide feedback on the engineering work covered in this FAQ, see the [Replication tracking task](https://github.com/xmtp/xmtpd/issues/118) in the [xmtpd repo](https://github.com/xmtp/xmtpd). ### Is the XMTP network decentralized? A testnet of the decentralized XMTP network was launched in early December 2024. All of the nodes in the `dev` and `production` XMTP network environments are still operated by [Ephemera](https://ephemerahq.com/) (the company) and powered by [xmtp-node-go](https://github.com/xmtp/xmtp-node-go). These nodes in the `dev` and `production` XMTP network environments operate in US jurisdiction in compliance with Office of Foreign Assets Control (OFAC) sanctions and Committee on Foreign Investment in the United States (CFIUS) export compliance regulations. Accordingly, IP-based geoblocking is in place for the following countries/territories: - Cuba - Iran - North Korea - Syria - The Crimea, Donetsk People's Republic, and Luhansk People's Republic regions of Ukraine ### Is XMTP a blockchain? The testnet of the decentralized XMTP network includes two distributed systems: - The XMTP broadcast network - The XMTP appchain, which is an L3 blockchain securing all metadata that require strict ordering. To learn more, see [Decentralizing XMTP](https://xmtp.org/vision/concepts/decentralizing-xmtp). The `dev` and `production` XMTP network environments do not use a blockchain. Nodes in these networks run software to store and transfer messages between blockchain accounts. For secure and reliable delivery of messages, the nodes participate in a consensus mechanism. ### Will I be able to run my own XMTP node? At this time, not everyone will be able to run an XMTP node in the production decentralized XMTP network. To learn more, see [XIP-54: XMTP network node operator qualification criteria](https://community.xmtp.org/t/xip-54-xmtp-network-node-operator-qualification-criteria/868). ### Does XMTP have a token? XMTP does not currently have a token. Disregard any information regarding airdrops or token sales. If and when an official token is introduced, announcements will be made exclusively through XMTP's official channels. # Section: fund-agents-apps ## fund-agents-apps/calculate-fees.mdx import Zoom from 'react-medium-image-zoom'; import 'react-medium-image-zoom/dist/styles.css'; import Mermaid from '../../components/Mermaid'; # Understand and calculate XMTP fees Use this guide to understand XMTP fees and how to calculate estimated XMTP fees for an app or agent. ## Understand XMTP fees To support a decentralized and sustainable network, XMTP operates on a usage-based fee model. **All XMTP network fees are paid in USDC** (USD Coin, a stablecoin pegged to the US dollar). The fees paid by apps and agents (payers) directly compensate the independent node operators who run the infrastructure, ensuring the network remains resilient, secure, and censorship-resistant. :::tip[How to estimate your costs] Network fees are estimated to cost $5 per 100,000 chat messages, inclusive of all fee types. - **If you know your message volume**, multiply by $5 per 100,000 to estimate your costs. For example, if your app or agent's message volume is 100,000 messages per day, your estimated cost is $5 per day or $150 per month. - **If you have not launched messaging**, we recommend assuming each monthly active messaging user sends 500 messages per month. For example, 10,000 monthly active messaging users sending an average of 500 messages per month per user will generate an estimated $250 per month in fees. ::: ### Fee types To understand XMTP fee types, it's helpful to understand XMTP's architecture, which uses a two-layer system: 1. **XMTP Broadcast Network** (offchain): A globally distributed network of nodes responsible for securely routing and delivering encrypted messages between users. 2. **XMTP App Chain** (onchain): A specialized blockchain that stores critical metadata, such as user identities, contacts, and group permissions. This onchain layer is essential for decentralized identity and ensuring that only the appropriate users can access conversations. ### Messaging fees Messages sent through the **XMTP Broadcast Network** incur these types of **messaging fees**: - **Base fee**: A flat per-message fee charged at a single global rate, not a rate per app or sender. - **Storage fee**: A fee charged per-byte-day of storage required. - **Congestion fee**: A dynamic fee computed by looking at the recent activity of an originator. Added only during periods of high network activity. The fee rates for the base fee, storage fee, and congestion fee are denominated in USDC and stored as constants in a smart contract. These rates are set and adjusted through protocol governance and remain constant for a specified period of time. Every message an app or agent sends through the XMTP Broadcast Network counts against its message allowance. These message types include: - App messages (text, reactions, replies) - Media attachments (charged by size, with a 1 MB cap). To send larger files and reduce fees, use a remote attachment to point to off-network storage (for example, IPFS or S3). - System messages (read receipts, typing indicators) To learn more, see [Envelope types](/protocol/envelope-types). Collected messaging fees are paid directly to the node operators who run the globally distributed nodes that power the XMTP Broadcast Network. #### Messaging fee payment flow This sequence diagram illustrates the messaging fee payment flow from payers to independent node operators using a series of [smart contracts](https://github.com/xmtp/smart-contracts/blob/main/src/settlement-chain/README.md). >PayerRegistry: 1. Allocates USDC to cover usage fees Note over PayerReportManager: Node operator submits payer usage reports PayerReportManager->>PayerRegistry: 2. Deducts fees from payer's balance PayerRegistry->>DistributionManager: 3. Transfers collected fees NodeOperator->>DistributionManager: 4. Claims share of fees DistributionManager->>NodeRegistry: 5. Verifies claimant is node owner NodeRegistry-->>DistributionManager: Returns node owner's address DistributionManager->>NodeOperator: 6. Transfers earned fees to node operator `} /> ### Gas fees Certain administrative operations involve transactions on the XMTP App Chain: - Group management updated (member additions, permission changes) - Identity updates - Payer-related updates To pay for these transactions, apps and agents need to maintain a gas reserve balance on the XMTP App Chain. The Funding Portal automatically manages funding this gas reserve for apps and agents based on their expected message volume. Collected gas fees are paid directly to the XMTP App Chain. Fees are used to maintain the App Chain. ## Calculate estimated XMTP fees As a rough estimate, an app or agent can expect to pay about **$5 per 100,000 messages** ($0.00005/message). This all-in fee for typical usage is based on the following key assumptions: - A 95/5 split between messaging and gas fees. Meaning, for every $5 in total fees: - $4.75 is for XMTP Broadcast Network messaging fees - $0.25 is for XMTP App Chain gas fees - An average message size of 1 KB (1024 bytes). - A message retention period of 90 days. - Normal network conditions (no congestion fees). Your actual costs may vary depending on your app's specific usage patterns. For example, sending smaller messages or using a shorter retention period will result in lower messaging fees. All pricing is onchain and fully auditable. For the most accurate estimates based on your specific usage, the XMTP Funding Portal provides a detailed breakdown of messaging fees, gas fees, and your historical and projected spend. ### Calculate estimated messaging fees The total messaging fee for a single message is the sum of its component fees: `total messaging fee = base fee + storage fee + congestion fee` The storage fee is calculated as follows: `storage fee = rate × message size in bytes × message retention in days` For example, a 1 KB text message stored for 90 days on the XMTP Broadcast Network would incur a messaging fee calculated as: `total messaging fee = base fee + (storage rate × 1024 bytes × 90 days) + congestion fee` The congestion fee is applied only during periods of high network activity to manage load. ### Calculate estimated gas fees Gas fees for transactions on the XMTP App Chain are calculated as follows: `gas fee = fee per unit of gas × amount of gas consumed by a transaction` This often works out to fractions of a US cent. The fee per unit of gas is set by the XMTP App Chain based on network activity. The total amount of gas consumed depends on the size of the transaction. ### Fees and network scale The network is designed for sustainability, not to accumulate profit. Fee revenue is used to cover network operational costs incurred by node operators. The messaging fee per message is expected to step down as the network achieves and sustains global message volume milestones. For example, at a volume of 1 billion messages per month, revenue is expected to cover operational costs. The specific schedule for these milestones is managed by protocol governance, with a current effective floor fee target of $5 per 100,000 messages. While the system is designed for fees to decrease over time, the global messaging fee can rise if network volume drops significantly, ensuring node operator sustainability. ## fund-agents-apps/fund-your-app.mdx import { Tabs } from '../../components/Tabs'; # Fund an app or agent to send messages with XMTP Use this guide to learn how to use the XMTP Funding Portal to fund an app or agent to send messages with XMTP. You can also use the portal to view usage and current and projected fees. Behind the scenes, the Funding Portal handles Payer Registry smart contract deposits, XMTP Broadcast Network messaging fee and XMTP App Chain gas payments, and all blockchain interactions. [Ephemera](https://ephemerahq.com/) currently hosts the XMTP Funding Portal UI as a community service. Stewardship will move to DUNA post-GA. The [smart contracts](https://github.com/xmtp/smart-contracts) used by the portal are fully decentralized and non-custodial. Full audits from [Trail of Bits](https://www.trailofbits.com/) and [Octane](https://www.octane.security/) will be published before paid messaging is enforced. ## Understand payer wallets A payer wallet is what an app or agent's XMTP Gateway Service uses to pay fees. You can: - [Self-fund a payer wallet](#self-fund-a-payer-wallet) - [Fund a payer wallet using another wallet](#fund-a-payer-wallet-using-another-wallet) For example, you might self-fund your payer wallet if it already has or can easily obtain USDC. Or, you might ask your finance team to fund your payer wallet from a company multi-sig. You might also use a wallet you control to support a different app or agent by funding its payer wallet. Self-fund a payer wallet Fund a payer wallet using another wallet ## Self-fund a payer wallet ### Step 1. Create the payer wallet The payer wallet must be: - A standard Ethereum-compatible wallet account - Non-custodial, meaning you control the private key (not a third-party service) - Able to sign and transact on the Base and XMTP App Chain networks - Able to hold and transfer USDC tokens You can create a payer wallet using common non-custodial wallet apps, such as Coinbase Wallet and MetaMask. Payer wallets can be funded by Ethereum EOAs and smart contract wallets. For example, a Gnosis Safe or any ERC-1271 wallet can deposit funds into a payer account using the Funding Portal. You'll need your payer wallet's private key when setting up your [XMTP Gateway Service](/fund-agents-apps/run-gateway). ### Step 2. Register the payer wallet 1. Use your payer wallet to connect to the XMTP Funding Portal → testnet link coming soon. 2. On the **Welcome** page, click **Use connected wallet**. 3. Open the drop-down menu in the upper right and click **Manage payers**. 4. Click the pencil icon to give your payer wallet a human-readable name. 5. Click the green check button to save. Your payer wallet is now a payer in the Payer Registry smart contract. ### Step 3: Fund the payer wallet with USDC on Base Fund your payer wallet with USDC on Base. Here are some sources you can use to acquire USDC: - Centralized exchanges: Binance, Coinbase, Kraken - Direct purchase: Circle, Coinbase - Business accounts: Circle business accounts for larger operations To get USDC on Base Sepolia for use with XMTP testnet, you can use [https://faucet.circle.com/](https://faucet.circle.com/), which provides 10 USDC per hour. ### Step 4: Allocate funds for messaging :::warning Once you fund a payer wallet, only the payer wallet can [withdraw](#step-6-withdraw-and-claim-funds) USDC from the messaging and gas fee allowance. - Withdrawals from the Payer Registry will be available after 48 hours and require a second transaction to claim. - Withdrawals from the XMTP App Chain that require bridging to Base will be available after 7 days and require a second transaction to claim. ::: The XMTP Funding Portal will accept only testnet USDC until **November 1, 2025**. Plan your testnet and mainnet funding accordingly. 1. Connect the payer wallet to the XMTP Funding Portal → testnet link coming soon. 2. In the drop-down menu in the upper right, be sure to select the payer wallet. 3. On the **Dashboard** page, click **Fund**. 4. Enter the amount of USDC you want to allocate from your payer wallet. 5. The XMTP Funding Portal automatically optimizes how the funds are allocated to cover messaging fees and gas fees. Expand the **Transaction details** area to view the details of the split. 6. Click **Continue**. 7. The **Depositing funds** screen displays. You can click **Cancel transaction** to attempt to cancel the transaction, if needed. Your payer wallet now has: - USDC allocated to your registered payer wallet in the Payer Registry smart contract. This allowance will be used to pay XMTP Broadcast Network messaging fees. - USDC bridged to your payer wallet on the XMTP App Chain. This balance will be used to pay XMTP App Chain gas fees. ### Step 5: Monitor your usage and allowance You can use the **Usage** panel on the XMTP Funding Portal **Dashboard** to review the number of messages sent by your app, as well as actual and projected fees. Before data can display in the Usage panel, you must have completed the following on the appropriate network (testnet or mainnet): 1. [Deployed your XMTP Gateway Service](/fund-agents-apps/run-gateway) 2. Updated your app to [use a compatible XMTP SDK](/fund-agents-apps/update-sdk) 3. Sent messages using your app We recommend funding an allowance for 3-6 months of estimated usage. If your allowance goes to zero, the Payer Registry smart contract rejects new messages sent to the XMTP Broadcast Network and returns an `INSUFFICIENT_PAYER_BALANCE` error. ### Step 6: Withdraw and claim funds You can use the payer wallet (and only the payer wallet) to withdraw USDC from the messaging and gas fee allowance at any time. - Withdrawals from the Payer Registry will be available after 48 hours and require a second transaction to claim. - Withdrawals from the XMTP App Chain that require bridging to Base will be available after 7 days and require a second transaction to claim. 1. Connect your payer wallet to the XMTP Funding Portal → testnet link coming soon. 2. On the **Dashboard** page, click **Withdraw**. 3. Enter the amount of USDC you want to withdraw from your messaging balance. Click **MAX** if you want to withdraw the maximum amount available. 4. Click **Request withdrawal**. 5. After the required wait time, return to the XMTP Funding Portal to complete your withdrawal. On the homepage, view the **Transaction** panel and locate your **Withdrawal** and **Bridge** transactions. The **Status** column should be set to **_Ready to withdraw_**. 6. Click **Ready to withdraw** to display the **Transaction details** panel. 7. Verify the withdrawal details and click **Claim USDC**. ## Fund a payer wallet using another wallet ### Step 1. Get the payer wallet address To fund an app using a wallet other than its payer wallet, you need the payer wallet address. ### Step 2. Select the payer wallet you want to fund 1. Use the wallet you want to use to fund the payer wallet to connect to the XMTP Funding Portal → testnet link coming soon. 2. On the **Welcome** page, click **Use other wallet**. 3. On the **Manage payer wallets** screen, enter the payer wallet address you want to fund and a human-readable display name. 4. Click the green check button to save. Your wallet can now fund the payer wallet. ### Step 3: Deposit USDC into your wallet Deposit USDC into your wallet on Base. This is the wallet you want to use to fund the payer wallet. Here are some sources you can use to acquire USDC: - Centralized exchanges: Binance, Coinbase, Kraken - Direct purchase: Circle, Coinbase - Business accounts: Circle business accounts for larger operations To get USDC on Base Sepolia for use with XMTP testnet, you can use [https://faucet.circle.com/](https://faucet.circle.com/), which provides 10 USDC per hour. ### Step 4: Allocate funds to the payer wallet :::warning Once you use your wallet to allocate funds to the payer wallet, only the payer wallet can [withdraw](#step-6-withdraw-and-claim-funds) USDC from the messaging fee allowance. ::: The XMTP Funding Portal will accept only testnet USDC until **November 1, 2025**. Plan your testnet and mainnet funding accordingly. 1. Use the wallet you funded in Step 3 to connect to the XMTP Funding Portal → testnet link coming soon. 2. In the drop-down menu in the upper right, be sure to select the payer wallet. 3. On the **Dashboard** page, click **Fund**. 4. Enter the amount of USDC you want to allocate from your wallet to the payer wallet. 5. The XMTP Funding Portal automatically optimizes how the funds are allocated to cover messaging fees and gas fees. Expand the **Transaction details** area to view the details of the split. 6. Click **Continue**. 7. The **Depositing funds** screen displays. You can click **Cancel transaction** to attempt to cancel the transaction, if needed. The payer wallet now has: - USDC allocated to the registered payer wallet in the Payer Registry smart contract. This allowance will be used to pay XMTP Broadcast Network messaging fees. - USDC bridged to the payer wallet on the XMTP App Chain. This balance will be used to pay XMTP App Chain gas fees. ## Troubleshooting ### Is there a testnet? Yes. The XMTP App Chain testnet and XMTP Broadcast Network testnet run smart contracts that are identical to those run on mainnet. You can dry-run allocating funds using Base Sepolia USDC and sending messages using these XMTP testnets. ### Signature rejected (MetaMask only) If you see a **Signature rejected** error in MetaMask, it can sometimes be caused by a stuck or out-of-sync **nonce** (a number that keeps track of your transaction order). To fix this: 1. Open MetaMask. 2. Click your **account icon** in the top right. 3. Go to **Settings → Advanced**. 4. Click **Reset Account**. This does not affect your funds or wallet. If the issue persists: - Check for any stuck or pending transactions in your wallet. - Try sending a new transaction with a **custom nonce** if needed. ### Bridge pending > 15 min Check Base status: [https://status.base.org/](https://status.base.org/). ### Message reverted Check for `INSUFFICIENT_PAYER_BALANCE`. ### Region restrictions Nodes in the XMTP testnet and mainnet that operate in US jurisdiction do so in compliance with Office of Foreign Assets Control (OFAC) sanctions and Committee on Foreign Investment in the United States (CFIUS) export compliance regulations. Accordingly, for these nodes, IP-based geoblocking is in place for the following countries/territories: - Cuba - Iran - North Korea - Syria - The Crimea, Donetsk People’s Republic, and Luhansk People’s Republic regions of Ukraine ## fund-agents-apps/get-started.md # Get started with funding an app or agent to send messages with XMTP Starting December 9, 2025, apps and agents must pay fees to send messages on the decentralized XMTP Broadcast Network. This guide provides a timeline and checklist to prepare your app or agent for this transition. ## Key production milestones - **October 15, 2025**: - The XMTP Gateway Service library is available in Go. - **→ Deploy your gateway service.** - **November 15 2025**: - All client SDKs and are compatible with mainnet. - **→ Update your client SDKs.** - XMTP payer wallets can be funded on mainnet. - **→ Fund your payer wallet.** - **December 9, 2025**: - All message traffic is routed through the decentralized network. - **→ Older client SDKs are incompatible.** ## Required tasks Complete these tasks by **December 9, 2025**, to ensure your app or agent can send messages on the decentralized XMTP Broadcast Network. 1. **From October 15**: Deploy the [XMTP Gateway Service](/fund-agents-apps/run-gateway) to enable your app or agent to send messages and pay fees on XMTP mainnet. 2. **From November 15**: Update your app or agent to use a [decentralization-ready XMTP SDK](/fund-agents-apps/update-sdk) to connect to your XMTP Gateway Service and [fund your app or agent](/fund-agents-apps/fund-your-app) using the XMTP Funding Portal. We recommend funding 3-6 months of estimated usage. ## Recommended next steps - Learn about [XMTP fees](/fund-agents-apps/calculate-fees). - Test your implementation: - Verify that your [XMTP Gateway Service](/fund-agents-apps/run-gateway) and [funding](/fund-agents-apps/fund-your-app) are working correctly. - [Set up monitoring](/fund-agents-apps/run-gateway#metrics-and-observability) and automated tests for your XMTP Gateway Service. ## fund-agents-apps/glossary.md # Glossary for funding an app or agent to send messages with XMTP ## Allowance Total amount of USDC allocated to pay for messaging and gas fees for a registered payer wallet. This includes: - USDC allocated to your registered payer wallet in the Payer Registry smart contract. This allowance will be used to pay XMTP Broadcast Network messaging fees. - USDC in your payer wallet on the XMTP App Chain. This balance will be used to pay XMTP App Chain gas fees. To learn more, see [Fund an app to send messages with XMTP](/fund-agents-apps/fund-your-app). ## Base An Ethereum L2 network developed by Coinbase on the OP Stack. Base processes transactions offchain and settles them on Ethereum. ## Congestion fee A dynamic offchain fee added during high network activity to manage load, determined by and paid to node operators. To learn more, see [Understand and calculate XMTP fees](/fund-agents-apps/calculate-fees). ## Message fee A fixed offchain micropayment paid to node operators for delivering a message. To learn more, see [Understand and calculate XMTP fees](/fund-agents-apps/calculate-fees). ## Node Registry A smart contract on Base that manages the list of authorized XMTP Broadcast Network nodes. To learn more, see [NodeRegistry.sol](https://github.com/xmtp/smart-contracts/blob/6ff95e20acdcfdbf932cb3254ad132daeb3e59e4/src/settlement-chain/NodeRegistry.sol) in the smart-contracts repo. ## Payer A payer is typically an app or agent that pays to use the XMTP Broadcast Network to send messages. ## Payer Registry A smart contract that holds and manages messaging balances for registered payer wallets. The Payer Registry enables: - **Pay-as-you-go messaging**: A developer registers their payer wallet and allocates USDC to it, creating a messaging balance to compensate node operators. - **Balance management**: Any wallet can deposit USDC to a registered payer wallet's messaging balance. Only the registered payer wallet can withdraw funds from its balance. - **Fee settlement**: The Payer Registry tracks usage, deducts fees from messaging balances, and enforces sufficient balance requirements for sending messages. To learn more, see [PayerRegistry.sol](https://github.com/xmtp/smart-contracts/blob/6ff95e20acdcfdbf932cb3254ad132daeb3e59e4/src/settlement-chain/PayerRegistry.sol#L38) in the XMTP smart-contracts repo. ## Payer wallet A non-custodial, Ethereum-compatible wallet that you register and use to allocate USDC to pay for your app's messaging fees. The payer wallet is the only wallet that can control its messaging balance in the Payer Registry, but any wallet can allocate funds to its messaging balance. To learn more, see [Fund an app to send messages with XMTP](/fund-agents-apps/fund-your-app). ## Storage fee An offchain micropayment paid to node operators based on the size (in bytes) of a stored message. ## Subgraph A real-time index that tracks PayerRegistry contract activity—such as who funded a messaging balance, how much was spent, and which messages incurred fees—enabling the XMTP Funding Portal to display historical and live fee data. ## XMTP App Chain An L3 blockchain built as an Arbitrum Orbit rollup that settles onto Base. It manages metadata requiring strict ordering through these smart contracts: - [`identity_update.go`](https://github.com/xmtp/xmtpd/blob/522d05f5a5d0499157635aba98c3f5b2556470d4/pkg/indexer/app_chain/contracts/identity_update.go): Tracks wallet addresses associated with each XMTP inbox - [`group_message.go`](https://github.com/xmtp/xmtpd/blob/522d05f5a5d0499157635aba98c3f5b2556470d4/pkg/indexer/app_chain/contracts/group_message.go): Manages group membership changes with guaranteed ordering Gas fees are charged for onchain transactions on the XMTP App Chain. These transactions are typically for group membership, identity, and payer-related updates. Fees are paid with USDC directly held by payer wallets on the XMTP App Chain. When you [fund your payer wallet](/fund-agents-apps/fund-your-app) using the XMTP Funding Portal, it automatically bridges an optimized percentage of the USDC funds to the XMTP App Chain to cover gas fees. ## XMTP Broadcast Network The offchain globally distributed network of nodes responsible for securely routing and delivering encrypted messages between users. Messaging fees support the operators of these nodes. ## XMTP Gateway Service A small client that acts as a proxy between your app and the payer wallet, and more specifically, the payer wallet's private key. Here's what the XMTP Gateway Service handles: - Security: The payer wallet's private key is sensitive information that shouldn't be in your app - The XMTP Gateway Service hosts this private key securely on your infrastructure - Your app never sees or handles the private key directly - Authorization: The XMTP Gateway Service can implement your app's authorization logic - It can verify that requests are coming from your legitimate users - It can rate limit requests - It can enforce your app's business rules - Fee management: The XMTP Gateway Service handles both types of fees: - Signs transactions on Base for paying messaging fees from the PayerRegistry contract - Signs transactions on the XMTP App Chain for paying gas fees from the payer wallet (Payers contract) To learn more, see [Run an XMTP Gateway Service](/fund-agents-apps/run-gateway). ## XMTP Settlement Chain An L3 blockchain built on Base that manages the decentralized network of nodes and payers and facilitates the settlement of fees between them. The core functionality revolves around registering nodes, submitting and settling fees for offchain network usage by payers, and distributing the collected revenue to the node operators and the protocol. To learn more, see the [README](https://github.com/xmtp/smart-contracts/blob/main/src/settlement-chain/README.md) for the XMTP Settlement Chain smart contracts. ## fund-agents-apps/run-gateway.mdx import Zoom from 'react-medium-image-zoom'; import 'react-medium-image-zoom/dist/styles.css'; import Mermaid from '../../components/Mermaid'; import { ConfigTable } from '../../components/ConfigTable'; # Run your XMTP Gateway Service An XMTP Gateway Service is a proxy that pays for and forwards messages to the XMTP Broadcast Network on behalf of your users. Think of it as your app or agent's payment gateway for messaging. Behind the scenes, your XMTP Gateway Service handles all the complexity of blockchain payments so your users (and you) don't have to think about it. - **For browser and mobile client apps**, you host your XMTP Gateway Service on your infrastructure because it contains your payer wallet's private key. - **For agents and Node.js apps**, there is no need to run a separate XMTP Gateway Service. The XMTP Gateway Service will be built into the XMTP Node and Agent SDKs. XMTP provides a standard implementation of an XMTP Gateway Service that you can extend to meet your unique needs. Once your XMTP Gateway Service is deployed, you will need to include the `payer_service_address` when creating any XMTP client in your app or agent. ## Get started Every app and agent needs an XMTP Gateway Service. ### For browser and mobile client apps Use the [example gateway service repository](https://github.com/xmtp/gateway-service-example) as a starting point to build your own custom gateway with authentication For detailed implementation steps, see [Deploy your XMTP Gateway Service](#deploy-your-xmtp-gateway-service). ### For agents and Node.js apps **No need to run a separate XMTP Gateway Service.** The XMTP Gateway Service will be built into the XMTP Node and Agent SDKs—_COMING SOON_. ```tsx [TypeScript] // Automatic gateway included, no separate service needed const client = await Client.create(wallet, { env: 'mainnet', }); ``` ## A minimal example This is all the code you need to create the most bare bones XMTP Gateway Service. The service will configure itself from command line flags or environment variables, and begin receiving traffic on ports `5050` and `5055`. This minimal example will authorize every request it receives with no limits. In a real production app, you will want to make sure that you only approve requests from your own users. ```go [Go] package main import ( "context" "log" "slices" "github.com/xmtp/xmtpd/pkg/gateway" ) func main() { // This will gather all the config from environment variables and flags gatewayService, err := gateway.NewGatewayServiceBuilder(gateway.MustLoadConfig()). Build() if err != nil { log.Fatalf("Failed to build gateway service: %v", err) } gatewayService.WaitForShutdown() } ``` ## Configure your XMTP Gateway Service You can provide the configuration options for your XMTP Gateway Service either directly in your code, via command line flag, or through environment variables. | Name | Required | Command line flag | Environment variable | Info | | ------------------------ | -------- | -------------------------------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Payer Private Key | `true` | `--payer.private-key` | `XMTPD_PAYER_PRIVATE_KEY` | The `secp256k1` private key of the Ethereum Account you have already funded in the Funding Portal. Used to sign transactions and pay fees from your payer allowance in the Payer Registry smart contract. | | App Chain RPC URL | `true` | `--contracts.app-chain.rpc-url` | `XMTPD_APP_CHAIN_RPC_URL` | The RPC URL of your Blockchain RPC provider's endpoint for XMTP Chain | | Settlement Chain RPC URL | `true` | `--contracts.settlement-chain.rpc-url` | `XMTPD_SETTLEMENT_CHAIN_RPC_URL` | The RPC URL of your Blockchain RPC provider's endpoint for the Base chain | | App Chain WSS URL | `true` | `--contracts.app-chain.wss-url` | `XMTPD_APP_CHAIN_WSS_URL` | The websocket URL of your Blockchain RPC provider's endpoint for XMTP Chain | | Settlement Chain WSS URL | `true` | `--contracts.settlement-chain.wss-url` | `XMTPD_SETTLEMENT_CHAIN_WSS_URL` | The websocket URL of your Blockchain RPC provider's endpoint for the Base chain | | Environment | `true` | `--contracts.environment` | `XMTPD_CONTRACTS_ENVIRONMENT` | The environment your XMTP Gateway Service will run in. Valid values are `anvil`, `testnet`, and `mainnet` | | Redis Connection String | `false` | `--redis.connection-string` | `XMTPD_REDIS_CONNECTION_STRING` | The connection string for your Redis instance | ### Metrics and observability If the XMTP Gateway Service is configured with the `--metrics.enable` flag, it will expose a [Prometheus](https://prometheus.io) endpoint at `/metrics` on port `8008` which can be ingested by any compatible monitoring system. ## Allocate funds for messaging The private key for your XMTP Gateway Service must belong to a payer wallet that has allocated funds for messaging in the XMTP Funding Portal. To learn more, see [Fund your app or agent to send messages with XMTP](/fund-agents-apps/fund-your-app). ## Authenticate users You can think of the XMTP Gateway Service as an extension of your client app. If you already have a system for authenticating user requests in your app, you should use it in your XMTP Gateway Service. For example, many apps use [JSON Web Tokens](https://jwt.io) (JWTs) to authenticate client requests to servers. You can use any authentication scheme you like to authenticate requests from your client to the XMTP Gateway Service. :::tip[IMPORTANT] - **Browser and mobile client apps** that need user authentication must customize the XMTP Gateway Service to integrate with their authentication system. - **Node.js apps and agents** that need user authentication will be able to use the XMTP Gateway Service provided by the XMTP Node SDK without customization. ::: ### In your client—_COMING SOON_ When creating an XMTP client, you will be able to optionally provide an `gatewayAuthTokenFetcher` as part of client configuration. The value returned by this function will be included in all client requests to the XMTP Gateway Service. The `fetchAuthToken` function will be called before the first request is made to the XMTP Gateway Service, and after any request to the XMTP Gateway Service fails due to a `PermissionDenied` error. {/* Commenting out b/c this functionality is not yet live #### Create a `gatewayAuthTokenFetcher` A Gateway Auth Token Fetcher provides a single function: `fetchAuthToken` that returns a string. :::code-group ```tsx [Agent] import type { GatewayAuthTokenFetcher } from '@xmtp/node-sdk'; const authTokenFetcher: GatewayAuthTokenFetcher = { fetchAuthToken: async () => { return 'my-auth-token'; }, }; ``` ```tsx [Browser] import type { GatewayAuthTokenFetcher } from '@xmtp/browser-sdk'; const authTokenFetcher: GatewayAuthTokenFetcher = { fetchAuthToken: async () => { // Get the auth token from your app somehow return 'my-auth-token'; }, }; ``` ```tsx [Node] import type { GatewayAuthTokenFetcher } from '@xmtp/node-sdk'; const authTokenFetcher: GatewayAuthTokenFetcher = { fetchAuthToken: async () => { return 'my-auth-token'; }, }; ``` ```tsx [React Native] import type { GatewayAuthTokenFetcher } from '@xmtp/react-native-sdk'; const authTokenFetcher: GatewayAuthTokenFetcher = { fetchAuthToken: async () => { return 'my-auth-token'; }, }; ``` ```kotlin [Kotlin] class MyAuthTokenFetcher : GatewayAuthTokenFetcher { override suspend fun fetchAuthToken(): String { return "my-auth-token" } } ``` ```swift [Swift] public struct MyAuthTokenFetcher: GatewayAuthTokenFetcher { public func fetchAuthToken() async throws -> String { return "my-auth-token" } } ``` ::: */} ### In the XMTP Gateway Service The `GatewayServiceBuilder` allows you to provide an `IdentityFn` that you can use to identify your users. The function is expected to return a `gateway.Identity` struct, which identifies the user in a way that is unique to your app. This identity will then be used for rate limiting, and will be passed to your `AuthorizePublishFn` as additional context. :::code-group ```go [IP address] // We provide a simple implementation that uses the client's IP address to identify users. For a production application, you should limit requests to only users actually authenticated in your application. package main import ( "context" "log" "slices" "github.com/xmtp/xmtpd/pkg/gateway" ) func main() { // This will gather all the config from environment variables and flags gatewayService, err := gateway.NewGatewayServiceBuilder(gateway.MustLoadConfig()). // The gateway service will use the IP address of the client as the identity by default Build() if err != nil { log.Fatalf("Failed to build gateway service: %v", err) } gatewayService.WaitForShutdown() } ``` ```go [JWT] package main import ( "context" "errors" "log" "github.com/golang-jwt/jwt/v5" "github.com/xmtp/xmtpd/pkg/gateway" ) const EXPECTED_ISSUER = "my-app.com" var ( ErrMissingToken = errors.New("missing JWT token") ErrInvalidToken = errors.New("invalid JWT token") ErrInvalidSignature = errors.New("invalid token signature") ) // jwtIdentityFn creates an identity function that verifies JWTs func jwtIdentityFn(publicKey []byte) gateway.IdentityFn { return func(ctx context.Context) (gateway.Identity, error) { authHeader := gateway.AuthorizationHeaderFromContext(ctx) if authHeader == "" { return gateway.Identity{}, gateway.NewUnauthenticatedError( "Missing JWT token", ErrMissingToken, ) } // Parse and verify the token token, err := jwt.ParseWithClaims( authHeader, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) { // Verify signing method if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok { return nil, gateway.NewPermissionDeniedError( "Invalid signing method", ErrInvalidSignature, ) } return publicKey, nil }, jwt.WithIssuer(EXPECTED_ISSUER), ) if err != nil { return gateway.Identity{}, gateway.NewPermissionDeniedError( "failed to validate token", err, ) } // Extract claims claims, ok := token.Claims.(*jwt.RegisteredClaims) if !ok || !token.Valid { return gateway.Identity{}, gateway.NewPermissionDeniedError( "failed to validate token", ErrInvalidToken, ) } userID, err := claims.GetSubject() if err != nil { return gateway.Identity{}, gateway.NewPermissionDeniedError( "failed to get subject from token", err, ) } // Return identity based on JWT claims return gateway.NewUserIdentity(userID), nil } } func main() { // In a real application, this would be a secure key loaded from environment/config publicKey := []byte("your-applications-public-key") gatewayService, err := gateway.NewGatewayServiceBuilder(gateway.MustLoadConfig()). WithIdentityFn(jwtIdentityFn(publicKey)). Build() if err != nil { log.Fatalf("Failed to build gateway service: %v", err) } gatewayService.WaitForShutdown() } ``` ```go [Rate limiting] package main import ( "context" "log" "time" "github.com/xmtp/xmtpd/pkg/gateway" "github.com/xmtp/xmtpd/pkg/gateway/authorizers" ) func main() { cfg := gateway.MustLoadConfig() redis := gateway.MustSetupRedisClient(context.Background(), cfg.Redis) authorizer := authorizers.NewRateLimitBuilder(). WithLogger(gateway.MustCreateLogger(cfg)). WithRedis(redis). // Set rate limits to 50 requests/minute and 250 requests/hour WithLimits(authorizers.RateLimit{ Capacity: 50, RefillEvery: time.Minute, }, authorizers.RateLimit{ Capacity: 250, RefillEvery: time.Hour, }). MustBuild() gatewayService, err := gateway.NewGatewayServiceBuilder(cfg). WithRedisClient(redis). WithAuthorizers(authorizer). Build() if err != nil { log.Fatalf("Failed to build gateway service: %v", err) } gatewayService.WaitForShutdown() } ``` ::: ## Authorize requests Now that you have an identity for the caller of your API, you can use it to authorize requests. We provide some helpers to handle common authorization patterns, such as rate limiting. You can add multiple authorizers to your XMTP Gateway Service. All authorizers will be called in parallel. The first authorizer (based on the order they are added) that returns `false` or an error will cause the request to be rejected. ```go [IP Allowlist] package main import ( "context" "log" "slices" "github.com/xmtp/xmtpd/pkg/payer" ) func main() { payerService, err := payer.NewPayerServiceBuilder(payer.MustLoadConfig()). WithAuthorizers(func(ctx context.Context, identity payer.Identity, req payer.PublishRequest) (bool, error) { // A simple authorization function that allows only the IP 127.0.0.1 allowedIPs := []string{"127.0.0.1"} if !slices.Contains(allowedIPs, identity.Identity) { return false, payer.ErrUnauthorized } return true, nil }). Build() // This will gather all the config from environment variables and flags if err != nil { log.Fatalf("Failed to build XMTP Gateway Service: %v", err) } err = payerService.Serve(context.Background()) if err != nil { log.Fatalf("Failed to serve XMTP Gateway Service: %v", err) } } ``` ## Deploy your XMTP Gateway Service :::tip[Example repo] You can start with the [example gateway service repository](https://github.com/xmtp/gateway-service-example). ::: 1. Fork or clone the repository. 2. Add your own authentication logic. 3. Configure your rate limits 4. Deploy your custom image on your infrastructure of choice, such as a container hosting service ($25-50/month minimum). ### Additional recommendations The system is able to run without any external dependencies, but we recommend configuring a Redis instance to use for nonce management and rate limiting. If your XMTP Gateway Service goes down, messages will queue until it comes back online. Build redundancy, if needed. ## Test your XMTP Gateway Service You can use the prebuilt Docker image for local development and testing: ```bash [Bash] docker run -p 5050:5050 -p 5055:5055 -e XMTPD_PAYER_PRIVATE_KEY=... xmtp/xmtpd-gateway:main ``` :::warning This pre-built image authorizes all requests without authentication. Never use it in production. ::: ### Test scenarios Here are some high priority scenarios to test: - Deploy and test XMTP Gateway Service - **Not needed for agents and Node.js apps** using the XMTP Node or Agent SDKs, which will provide a built-in XMTP Gateway Service. - Verify authentication works properly - Test with expected message volumes - Monitor resource usage and costs - Simulate failure scenarios You can test on Base Sepolia, App Chain testnet, and Broadcast Network testnet. They all run identical contracts so you can dry-run funding and messaging. To get testnet USDC, use [https://faucet.circle.com/](https://faucet.circle.com/), which provides 10 USDC per hour. ### Testing timeline **Until November 1**: Testnet-only period - Use testnet USDC (no real cost) - Validate XMTP Gateway Service setup - Test authentication flows - Ensure infrastructure scales **After November 1**: Full mainnet testing - Funding Portal accepts real USDC - Production environment testing - Fee validation with real funds - Final integration checks ## XMTP Gateway Service flows The following sequence diagrams illustrate the XMTP Gateway Service's role in the following key flows. ### Flow for sending a message to the XMTP Broadcast Network >Gateway Service: Request to send message Gateway Service->>Gateway Service: Verify user authorization Gateway Service->>Gateway Service: Sign with payer wallet key Gateway Service->>XMTP Broadcast Network: Send message and pay network message fees alt Sufficient funds in Payer Registry XMTP Broadcast Network-->>App: Message sent else Insufficient funds in Payer Registry XMTP Broadcast Network-->>App: Message not sent end `} /> ### Flow for sending a message to the XMTP App Chain >Gateway Service: Request to send message Gateway Service->>Gateway Service: Verify user authorization Gateway Service->>Gateway Service: Sign with payer wallet key Gateway Service->>XMTP App Chain: Send message and pay App Chain gas fee alt Sufficient funds XMTP App Chain-->>App: Message sent else Insufficient funds XMTP App Chain-->>App: Message not sent end `} /> ## fund-agents-apps/update-sdk.md # Update your app or agent to use an XMTP SDK with Gateway Service support Starting on November 15, 2025, you'll be able to update your app or agent to use an XMTP SDK version that supports XMTP Gateway Service helpers, enabling your app or agent to communicate with your XMTP Gateway Service. The minimum supported SDK version for each platform will be: - Agent SDK v2.0.0 - Browser SDK v6.0.0 - Node SDK v5.0.0 - React Native SDK v6.0.0 - Kotlin SDK v5.0.0 - Swift SDK v5.0.0 ## FAQ ### Will updating to an SDK that uses the decentralized network impact performance and network speed? Most SDK actions will have comparable performance on the decentralized network. All reads (`sync*`, `stream*`, etc.) and the most common writes (`conversation.send`) will have similar performance. However, the following actions will have an estimated ~500ms slowdown: - Registering a new identity to the network (first-time client setup) - Adding members to a group - Updating group metadata ### Will existing local databases persist after updating my SDK? Local databases will be fully migrated. No messages will be lost during the transition. ### Can I update my app or agent to use the new SDK version starting on November 15, or do I need to wait until December 9? Yes, you can use the new SDK version starting on November 15. The SDK versions released on November 15 will work in backward compatibility mode with the centralized network. Once you upgrade and have your [Gateway Service](/fund-agents-apps/run-gateway) properly set up, the transition on December 9 will happen automatically. On December 9, the centralized network will start returning a special error code that tells clients to switch over to the decentralized network. If you're already using the November 15 SDK version, this switch will happen automatically without requiring any action on your part. # Summary This documentation contains 62 files from 4 sections.