# XMTP Full Documentation ## 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. ## pages/doc-feedback.md # Documentation feedback We’re excited to share documentation for the latest version of XMTP. We continuously release new and updated documentation. Not seeing the specific documentation you need? [Open an issue](https://github.com/xmtp/docs-xmtp-org/issues/new/choose). We ❤️ doc requests. Onward builders! 🫡 ## pages/terms.md --- description: "We are pleased to license much of the documentation on this site under terms that explicitly encourage people to take, modify, reuse, re-purpose, and remix this content as they see fit." --- # Terms of service Our content licensing policies are based on those of the [Google Developer](https://developers.google.com/site-policies) website. We are pleased to license much of the documentation on `docs.xmtp.org` under terms that explicitly encourage people to take, modify, reuse, re-purpose, and remix this content as they see fit. You will find the following notice at the bottom of pages on `docs.xmtp.org`: > CC BY 4.0 This means that except as otherwise noted, page content is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/) and code samples are licensed under the [MIT License](http://opensource.org/licenses/MIT). When you see a page with this notice, you are free to use [nearly everything](#what-is-not-licensed) on the page in your own creations. For example, you could quote the text in a book, cut-and-paste sections to your blog, record it as an audiobook for the visually impaired, or even translate it into Swahili. Really. That's what open content licenses are all about. We just ask that you give us [attribution](#attribution) when you reuse our work. If you have any questions about these terms of service, post to the [XMTP Community Forums](https://community.xmtp.org/). ## What is _not_ licensed? We say “nearly everything” as there are a few simple conditions that apply. Trademarks and other brand features are not included in this license. In some cases, a page might include content consisting of images, audio or video material, or a link to content on a different webpage (such as videos or slide decks). This content is not covered by the license, unless specifically noted. ## Attribution Proper attribution is required when you reuse or create modified versions of content that appears on a page made available under the terms of the Creative Commons Attribution license. The complete requirements for attribution can be found in section 3 of the [Creative Commons legal code](https://creativecommons.org/licenses/by/4.0/legalcode). In practice, we ask that you provide attribution to XMTP to the best of the ability of the medium in which you are producing the work. There are several typical ways in which this might apply: #### Exact reproductions If your online work _exactly reproduces_ text or images from this site, in whole or in part, please include a paragraph at the bottom of your page that reads: >Portions of this page are reproduced from work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). Also, please link back to the original source page so that readers can refer to it for more information. #### Modified versions If your online work shows _modified_ text or images based on the content from this site, please include a paragraph at the bottom of your page that reads: >Portions of this page are modifications based on work created and shared by XMTP and used according to terms described in the [Creative Commons 4.0 Attribution License](https://creativecommons.org/licenses/by/4.0/). Again, please link back to the original source page so that readers can refer to it for more information. This is even more important when the content has been modified. #### Other media If you produce non-hypertext works, such as books, audio, or video, we ask that you make a best effort to include a spoken or written attribution in the spirit of the messages above. ## pages/upgrade-from-legacy-V2.md # Upgrade from a legacy XMTP V2 SDK to a stable XMTP V3 SDK This document is for you if you are upgrading from a legacy XMTP V2 SDK. Legacy XMTP V2 SDKs include: - JavaScript SDK vx.x.x - React Native SDK <v3 - Android SDK <v3 - iOS SDK <v3 :::tip[Upgrading from a legacy XMTP V3 SDK?] Legacy XMTP V3 SDKs include Browser SDK v0.x.x-<v1.1.4, Node SDK v0.x.x-<v1.0.5, React Native SDK v3.x.x-<v4.0.2, Android SDK v3.x.x-<v4.0.3, and iOS SDK v3.x.x-<v4.0.4. To learn how to upgrade to stable XMTP V3, see [Upgrade from a legacy XMTP V3 SDK](/upgrade-from-legacy-V3). ::: XMTP V3 provides support for the features expected by modern messaging apps while laying a strong foundation for the future. This is a stable and performant release that will allow for new features and is not expected to receive breaking changes in the near-term. As developers on V2 explore XMTP V3, many have found that its new architecture provides an opportunity to rethink and refine their approach to messaging. While upgrading from V2 is possible, some teams chose to rebuild with V3 from the start to take full advantage of its performance, security, and scalability improvements. Evaluate your needs and consider the path that makes the most sense for your app. :::info[Key takeaways] - **Most core methods from V2 work in a similar way in V3**, with some notable differences that are covered in this document. - **Primary XMTP identifier is now an inbox ID, not an Ethereum address**. As covered in this document, this inbox can have a list of identities including Ethereum addresses as well as other types in the future, such as Passkeys and Bitcoin**. - ⛔️ **Rolling brownouts of the V2 network start on May 1, 2025. V2 will be deprecated on June 1, 2025**, after which all V2 conversations and messages will become read-only. To learn more, see [XIP-53: XIP V2 deprecation plan](https://community.xmtp.org/t/xip-53-xmtp-v2-deprecation-plan/867). Users will still be able to access their V2 communications in read-only format using [https://legacy.xmtp.chat/](https://legacy.xmtp.chat/). ::: ## Upgrade to XMTP V3 🟢 For mobile apps, **upgrade to an XMTP mobile SDK >=v4.0.4** following the detailed instructions in these release notes: - [React Native](https://github.com/xmtp/xmtp-react-native/releases/tag/v4.0.2) - [Kotlin](https://github.com/xmtp/xmtp-android/releases/tag/4.0.3) - [Swift](https://github.com/xmtp/xmtp-ios/releases/tag/4.0.4) 🟢 For web apps, **upgrade to an XMTP web SDK >=v1.1.4** or **upgrade to an XMTP node SDK >=v1.0.5** following the detailed instructions in these release notes: - [Browser](https://github.com/xmtp/xmtp-js/releases/tag/%40xmtp%2Fbrowser-sdk%401.1.4) - [Node](https://github.com/xmtp/xmtp-js/releases/tag/%40xmtp%2Fnode-sdk%401.0.5) - For additional guidance, try [xmtp.chat](https://xmtp.chat/), an interactive developer tool and chat app built with XMTP V3. 🟢 For agents, **explore [example agents](https://github.com/ephemeraHQ/xmtp-agent-examples)** built with the XMTP Node SDK >=v1.0.0. ## Primary XMTP identifier is now an inbox ID, not an Ethereum address XMTP is evolving from using Ethereum account addresses (0x...) as the primary identifier to an inbox-based identity model. This change allows for broader support of different authentication mechanisms, including the currently supported [Externally Owned Accounts (EOAs) and Smart Contract Wallets (SCWs)](/inboxes/create-a-signer), as well as future support for Passkeys and other identity types. Instead of assuming an Ethereum address as the unique identifier, developers should default to using the `inboxId`, where possible. An `inboxId` has a list of identity objects that explicitly includes the identity type (kind) and identifier. Some identity types, like Passkeys, do not have an associated onchain address, so using the `inboxId` provides a consistent way to identify users across different authentication methods. For example: ```json [ { "kind": "ETHEREUM", "identifier": "0x1234567890abcdef1234567890abcdef12345678", }, { "kind": "PASSKEY", // not yet supported; provided as an example only. "identifier": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMk", } ] ``` This change ensures that XMTP identities are more extensible and adaptable, accommodating future improvements in authentication methods while maintaining backward compatibility for Ethereum-based accounts. ### Example: Supporting multiple identity types With this new model, an app can now distinguish different identity types when creating a signer. ```tsx function createSigner(account): Signer { return { getIdentity: async () => ({ kind: account.isSCW ? "ETHEREUM" : "PASSKEY", // Passkeys are not yet supported; provided as an example only. identifier: account.address || account.passkeyId, }), signMessage: async (message) => { return account.signMessage(message); }, getChainId: account.isSCW ? () => BigInt(8453) : undefined, getBlockNumber: account.isSCW ? () => undefined : undefined, }; } ``` ### Before: Using getAddress() Previously, developers used `getAddress()` to retrieve an account’s Ethereum address: ```tsx const signer: Signer = { getAddress: () => "0x123...", signMessage: async (message) => { // return signed message }, }; ``` While this approach worked for EOAs, it assumed that all accounts were Ethereum addresses and did not allow for other identity types. ### After: Using getIdentity() Now, `getIdentity()` returns an identity object, allowing for multiple types of accounts: ```tsx const signer: Signer = { getIdentity: () => ({ kind: "ETHEREUM", // Identity type [ETHEREUM, PASSKEY (passkeys are not yet supported), etc.] identifier: "0x123...", // Account identifier }), signMessage: async (message) => { // return signed message }, }; ``` ### Before: newConveration() Previously, developers used an Ethereum address to create a new DM conversation: ```tsx const dm = await alix.conversations.findOrCreateDm(bo.address); ``` ### After: newConveration() Now, developers can use `inboxId` to create a new DM conversation because with the new flexible identity model, they cannot rely on the existence of an Ethereum address. ```tsx const dm = await alix.conversations.findOrCreateDm(bo.inboxId); ``` ## Core methods from V2 work in a similar way in V3 Most core methods from V2, such as `newConversation`, `list`, `stream`, and `streamAllMessages`, work in a similar way in V3. However, key differences include: - `newConversation` no longer takes addresses, but rather inbox IDs, as covered in [Primary XMTP identifier is now an inboxId, not an Ethereum address](#primary-xmtp-identifier-is-now-an-inbox-id-not-an-ethereum-address) - Prior to V3, a conversation could represent a V1 or V2 conversation. In V3, a conversation can represent a group chat or direct message (DM). To learn more, see [Build a chat inbox](/inboxes/pick-an-sdk). ## Local databases mean no need to export/import topic data - In V2, we needed to manually create and manage a local database for performance. Additionally, when moving across different SDKs, you could export a topic and then import it into the other SDK to shorten the performance loop. This was particularly useful when handling push notifications in React Native apps to increase the speed of exporting and importing a conversation across SDKs. - In V3, a client automatically creates and manages a local database per installation, so performance issues are no longer an issue. When moving across installations, use message history to move data between installations. In the case of React Native push notifications, the database is accessible from both the native layer and React Native. ## Loading messages - In V2, we used `listBatchMessages` to load all messages across conversations to help performantly load a conversation list in descending order by last message. - In V3, because there is a local database, we can simply use `list()`, which returns a list of conversations in descending order by last message, or by a conversation's `createdAt` value if it has no messages. ## Push notification differences ### Decryption - In V1 and V2, we used intros V1 (`fromIntro`) and invites V2 (`fromInvite`) to decrypt invites and intros. - In V3, new conversations are called welcomes, and we use `fromWelcome` to decrypt them. ### Getting topic IDs - In V2, we used `/xmtp/0/invite-$address/proto` to get the topic identifier for a new conversation. - In V3, we use `/xmtp/mls/1/w-$installationId/proto` to get the topic identifier for a new group chat or DM conversation. ## Installation-specific private keys In V2, we had a `privateKeyBundle` that could be exported and imported into any SDK to create a client. This allowed users to migrate their identity across SDKs without re-signing with their wallet. In V2, we also had a consistent identity key that we used to sign and verify messages, and their static nature could make them a vulnerability over time. In V3, we have installation-specific key bundles that are stored securely in the managed local database and rotated periodically, which adds a significant layer of security. This means that after a V3 client is created, it is as simple as calling the `build` method with an address and a `dbEncryptionKey`, which retrieves the appropriate key bundle from the local database. With the added security of installation-based keys, it is no longer possible to export and import them into other clients. Calling create will create a new installation if a local database is not already present. Provided the same wallet is used for signing, your `inboxID` will be the same across all installations. ### Create a client - In V2, `client.createFromKeyBundle` - In V3, `client.build` ### Sign a message - In V2, `client.privateKeyBundle.sign(message)` - In V3, `client.signWithInstallationKey(message)` ### Verify a signature - In V2, `Signature.verify(signature)` - In V3, `client.verifyInstallationSignature(message, signature, installationId)` ## Managing consent - In V2, we managed consent via `client.contacts.consentList`. - In V3, we can manage consent via `client.preferences.getInboxIdConsent(inboxId)`. However, we recommend that you now manage consent at the conversation level by `conversationId`. To learn more, see [Support user consent preferences](/inboxes/user-consent/support-user-consent#support-user-consent-preferences-to-provide-spam-free-inboxes). ## Summary of notable changes | Purpose | V2 method | V3 equivalent | | --- | --- | --- | | Create a new conversation | `findOrCreateDm(bo.address);` | `findOrCreateDm(bo.inboxId);` | | Loading messages | `listBatchMessages` | `list()` | | Push notification decryption | `fromInvite`, `fromIntro` | `fromWelcome` | | Get topic IDs for push notifications | `/xmtp/0/invite-$address/proto` | `/xmtp/mls/1/w-$installationId/proto` | | Create client | `client.createFromKeyBundle` | `client.build` | | Sign a message | `client.privateKeyBundle.sign(message)` | `client.signWithInstallationKey(message)` | | Verify a signature | `Signature.verify(signature)` | `client.verifyInstallationSignature(message, signature, installationId)` | | Manage consent | `client.contacts.consentList` | `client.preferences.getInboxIdConsent(inboxId)` | ## FAQ ### Can I use a V2 identity in an app built with V3? A V2 identity can access V2 functionality only. To access V3 functionality, you must create a new V3 identity. In V2, creating an identity required two signatures. In V3, we only need one signature. For example: ```kotlin // Database encryption key for the local database. const databaseEncryptionKey = new Uint8Array([ 233, 120, 198, 96, 154, 65, 132, 17, 132, 96, 250, 40, 103, 35, 125, 64, 166, 83, 208, 224, 254, 44, 205, 227, 175, 49, 234, 129, 74, 252, 135, 145, ]) // Creating a new client const xmtpClient = await Client.create(signer, { env: 'production', dbEncryptionKey: databaseEncryptionKey, // Required for V3 }) ``` ### Can I use V2 consent preferences in an app built with V3? Because V2 conversations are not automatically ported to V3, V2 consent preferences are less useful in a V3 app. So, when starting V3 conversations, you may need to approve or reject certain users again. ### Can I access V2 conversations and messages in an app built with V3? V3 delivers an enhanced encryption scheme that is even more secure than V2, laying the groundwork for decentralizing the network. While the difference in encryption schemes makes it challenging to import V2 conversations and messages into an app built with V3, this tradeoff establishes a stronger foundation for security, privacy, and decentralization. We recommend that apps upgrade directly to V3, giving people access to a pure V3+ messaging experience. To ensure continuity, users will still be able to access their V2 communications in read-only format using [https://legacy.xmtp.chat/](https://legacy.xmtp.chat/). ### Can I use V2 conversations to seed the conversation list in my app built with V3? While we don’t recommend it, you can store V2 conversations in a local database that you manage, and then, on the first install, your app can try to create V3 DM conversations using the peer addresses in the V2 conversations. If you go this route, ensure that you store the **allowed** V2 conversations only so you don’t port over spam. We don’t recommend using this approach because not all of the peer addresses may be reachable on the V3 network, which means that only some conversations would be seeded. ## pages/upgrade-from-legacy-V3.md # Upgrade from a legacy XMTP V3 SDK to a stable XMTP V3 SDK This document is for you if you are upgrading from a legacy XMTP V3 SDK. Legacy XMTP V3 SDKs include: - Browser SDK v0.x.x-<v1.1.4 - Node SDK v0.x.x-<v1.0.5 - React Native SDK v3.x.x-<v4.0.2 - Android SDK v3.x.x-<v4.0.3 - iOS SDK v3.x.x-<v4.0.4 :::tip[Upgrading from a legacy XMTP V2 SDK?] Legacy XMTP V2 SDKs include JavaScript SDK vx.x.x, React Native SDK <v3, Android SDK <v3, and iOS SDK <v3. To learn how to upgrade to stable XMTP V3, see [Upgrade from a legacy XMTP V2 SDK](/upgrade-from-legacy-V2). ::: XMTP V3 provides support for the features expected by modern messaging apps while laying a strong foundation for the future. This is a stable and performant release that will allow for new features and is not expected to receive breaking changes in the near-term. ## Upgrade to XMTP V3 stable The process to upgrade an app built with a legacy XMTP V3 SDK to a stable V3 SDK is designed to be straightforward, with most functions in stable V3 working as they did in legacy V3. However, there are some notable differences, which we cover here and in these release notes: 🟢 For mobile apps, **upgrade to an XMTP mobile SDK >=v4.0.4** following the detailed instructions in these release notes: - [React Native](https://github.com/xmtp/xmtp-react-native/releases/tag/v4.0.2) - [Kotlin](https://github.com/xmtp/xmtp-android/releases/tag/4.0.3) - [Swift](https://github.com/xmtp/xmtp-ios/releases/tag/4.0.4) 🟢 For web apps, **upgrade to an XMTP web SDK >=v1.1.4** or **upgrade to an XMTP node SDK >=v1.0.5** following the detailed instructions in these release notes: - [Browser](https://github.com/xmtp/xmtp-js/releases/tag/%40xmtp%2Fbrowser-sdk%401.1.4) - [Node](https://github.com/xmtp/xmtp-js/releases/tag/%40xmtp%2Fnode-sdk%401.0.5) - For additional guidance, try [xmtp.chat](https://xmtp.chat/), an interactive developer tool and chat app built with XMTP V3. 🟢 For agents, **explore [example agents](https://github.com/ephemeraHQ/xmtp-agent-examples)** built with the XMTP Node SDK >=v1.0.0. ## Primary XMTP identifier is now an inbox ID, not an Ethereum address XMTP is evolving from using Ethereum account addresses (0x...) as the primary identifier to an inbox-based identity model. This change allows for broader support of different authentication mechanisms, including the currently supported [Externally Owned Accounts (EOAs) and Smart Contract Wallets (SCWs)](/inboxes/create-a-signer), as well as future support for Passkeys and other identity types. Instead of assuming an Ethereum address as the unique identifier, developers should default to using the `inboxId`, where possible. An `inboxId` has a list of identity objects that explicitly includes the identity type (kind) and identifier. Some identity types, like Passkeys, do not have an associated onchain address, so using the `inboxId` provides a consistent way to identify users across different authentication methods. For example: ```json [ { "kind": "ETHEREUM", "identifier": "0x1234567890abcdef1234567890abcdef12345678", }, { "kind": "PASSKEY", // not yet supported; provided as an example only. "identifier": "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyAhIiMk", } ] ``` This change ensures that XMTP identities are more extensible and adaptable, accommodating future improvements in authentication methods while maintaining backward compatibility for Ethereum-based accounts. ### Example: Supporting multiple identity types With this new model, an app can now distinguish different identity types when creating a signer. ```tsx function createSigner(account): Signer { return { getIdentity: async () => ({ kind: account.isSCW ? "ETHEREUM" : "PASSKEY", // Passkeys are not yet supported; provided as an example only. identifier: account.address || account.passkeyId, }), signMessage: async (message) => { return account.signMessage(message); }, getChainId: account.isSCW ? () => BigInt(8453) : undefined, getBlockNumber: account.isSCW ? () => undefined : undefined, }; } ``` ### Before: Using getAddress() Previously, developers used `getAddress()` to retrieve an account’s Ethereum address: ```tsx const signer: Signer = { getAddress: () => "0x123...", signMessage: async (message) => { // return signed message }, }; ``` While this approach worked for EOAs, it assumed that all accounts were Ethereum addresses and did not allow for other identity types. ### After: Using getIdentity() Now, `getIdentity()` returns an identity object, allowing for multiple types of accounts: ```tsx const signer: Signer = { getIdentity: () => ({ kind: "ETHEREUM", // Identity type [ETHEREUM, PASSKEY (passkeys are not yet supported), etc.] identifier: "0x123...", // Account identifier }), signMessage: async (message) => { // return signed message }, }; ``` ### Before: newConveration() Previously, developers used an Ethereum address to create a new DM conversation: ```tsx const dm = await alix.conversations.findOrCreateDm(bo.address); ``` ### After: newConveration() Now, developers can use `inboxId` to create a new DM conversation because with the new flexible identity model, they cannot rely on the existence of an Ethereum address. ```tsx const dm = await alix.conversations.findOrCreateDm(bo.inboxId); ``` ## pages/index.mdx import { CustomHomePage } from "../components/CustomHomePage"; import "../styles.css"; <CustomHomePage.Root> <CustomHomePage.Headline>Build with XMTP</CustomHomePage.Headline> <CustomHomePage.Subhead> The largest and most secure decentralized messaging network </CustomHomePage.Subhead> <CustomHomePage.TileGrid> <CustomHomePage.Tile href="/upgrade-from-legacy-V2" title="Upgrade to XMTP V3" description="Get tips for upgrading from a legacy SDK to a stable XMTP V3 SDK" icon="🚀" /> <CustomHomePage.Tile href="/intro/intro" title="Intro to XMTP" description="Get an intro to XMTP and why devs choose it to build messaging apps" icon="🤝" /> <CustomHomePage.Tile href="/inboxes/pick-an-sdk" title="Build chat inboxes" description="Build standalone inbox apps with 1:1 and group chats built with MLS" icon="📥" /> <CustomHomePage.Tile href="https://xmtp.chat/" title="Build with xmtp.chat" description="Build an inbox app with the help of this interactive developer tool and chat app" icon="🧑🏽💻" isExternal={true} /> <CustomHomePage.Tile href="https://github.com/ephemeraHQ/xmtp-agent-examples" title="Build agents" description="Explore examples of agents built with the XMTP Node SDK" icon="🤖" isExternal={true} /> <CustomHomePage.Tile href="/network/run-a-node" title="Run an XMTP node" description="Learn about network nodes and how to become a registered node operator" icon="🕸️" /> <CustomHomePage.Tile href="/protocol/security" title="Learn protocol concepts" description="Dive deeper into XMTP by learning about key protocol concepts" icon="🧠" /> <CustomHomePage.Tile href="/intro/xips" title="Review XIPs" description="Read and provide feedback on the latest XMTP Improvement Proposals" icon="⚒️" /> </CustomHomePage.TileGrid> </CustomHomePage.Root> ## pages/privacy.md # Privacy policy `docs.xmtp.org` does not use cookies and does not collect any personal data or personally-identifiable information (PII). The site does use Plausible analytics to track overall trends in website traffic. Plausible does not track individual visitors. All analytics data is aggregated data only and it contains no personal information. The site uses Plausible analytics for the sole purpose of helping site contributors understand how to best deliver content that serves the needs of the XMTP community. To learn more about the data Plausible tracks, see the [Plausible Data Policy](https://plausible.io/data-policy). If you have any questions about this privacy policy, post to the [XMTP Community Forums](https://community.xmtp.org/). ## pages/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. <iframe width="560" height="315" src="https://www.youtube.com/embed/g6I9qXOkDMo?si=o5pD2xwa_yynoP5s" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> 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. ## 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. ## pages/protocol/signatures.md --- 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. <img width="400" src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/authen-to-inbox.PNG" alt="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.  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](/inboxes/user-consent/user-consent). ## pages/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.com/) - [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](/inboxes/pick-an-sdk). ### 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? <iframe width="560" height="315" src="https://www.youtube.com/embed/hNlby-SfPzw?si=V7LqaBxk-i4xhbNC" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> ### 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 XMTP’s blockchain and node databases: Currently, encrypted payloads are stored indefinitely. In the coming year, a retention policy will be added. This retention policy would represent a minimum retention period, not a maximum. For example, a retention policy may look something like the following, though specifics are subject to change: - One year for messages - Indefinite storage for account information and personal preferences The team is researching a way to provide this indefinite storage and have it scale forever. - If research shows that it's possible, we'll share a plan for how it will be achieved. - If research shows that it isn't possible, we'll share a plan that shows how retention periods will provide a permanent solution to storage scaling. Have questions or feedback regarding message storage and retention? Post to the [XMTP Community Forums](https://community.xmtp.org/c/development/ideas/54). #### 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. ### 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](/inboxes/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](/inboxes/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](/inboxes/pick-an-sdk), 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/ephemeraHQ/convos-app), open-sourced for the entire XMTP network. ## pages/intro/intro.md # What is 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. ## Why should I 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 inboxes** In any open and permissionless messaging ecosystem, spam is an inevitable reality, and XMTP is no exception. However, with XMTP [user consent preferences](/inboxes/user-consent/user-consent), developers can give their users spam-free inboxes 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. - Try the [Convos](https://www.convos.org/open) messaging app. Convos is open source courtesy of [Ephemera](https://ephemerahq.com/), the company stewarding the development and adoption of 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. ## pages/intro/build-with-llms.md # Use XMTP documentation with AI coding assistants To make it easier to use AI coding agents to build with XMTP, you can find an `llms-full.txt` file at [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). This `llms-full.txt` file includes all XMTP documentation in a single plain text file for easy parsing by AI agents. If you’re using an LLM tool that allows custom context, you can upload or point to `llms-full.txt` to enhance your AI coding experience with XMTP. ## pages/intro/xips.md # 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). ## pages/inboxes/group-metadata.md # 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](/inboxes/create-conversations/#create-a-new-group-chat), and get and update metadata using these methods. ## 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 [Node] 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") ``` ::: ## pages/inboxes/create-a-client.mdx # Create an XMTP client Create an XMTP client that can use the signing capabilities provided by the [signer](/inboxes/create-a-signer). This signer links the client to the appropriate EOA or SCW. ## 🎥 walkthrough: Create a client This video provides a walkthrough of creating a client, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. <iframe width="560" height="315" src="https://www.youtube.com/embed/kdUP1ZaauNI?si=ETuEbvwbKx4bjtwc" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen ></iframe> ## Create a 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, options, // optional ); ``` ```tsx [Node] import { Client, type Signer } from "@xmtp/node-sdk"; import { getRandomValues } from "node:crypto"; // create a signer const signer: Signer = { /* ... */ }; // this value should be generated once per installation and stored securely const encryptionKey = getRandomValues(new Uint8Array(32)); const client = await Client.create( signer, encryptionKey, options, // optional ); ``` ```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 ) ``` ::: When an app first calls `Client.create()`, a client creates a local database to manage messaging between the app and the network. In subsequent calls, it loads the existing database. The database is encrypted using the key provided when creating the client. To learn more about database operations, see the [XMTP MLS protocol spec](https://github.com/xmtp/libxmtp/blob/main/xmtp_mls/README.md). ### 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"; /** * 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; /** * Allow configuring codecs for additional content types */ codecs?: ContentCodec[]; /** * Path to the local DB */ dbPath?: string; /** * Encryption key for the local DB */ dbEncryptionKey?: Uint8Array; /** * 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; }; ``` ```tsx [Node] import type { ContentCodec } from "@xmtp/content-type-primitives"; type ClientOptions = { /** * Specify which XMTP environment to connect to. (default: `dev`) */ env?: "local" | "dev" | "production"; /** * 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; /** * Path to the local DB */ dbPath?: 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; }; ``` ```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' /** * 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> | 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, ) } ``` ```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 } 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? } ``` ::: ### XMTP network environments XMTP provides developer and production network environments. These networks are completely separate and not interchangeable. For example, an XMTP identity on the developer 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 developer 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 developer network by default. Set your client's network environment using the appropriate client option. ::: The production network is configured to store messages indefinitely. XMTP may occasionally delete messages and identities from the developer network, and will provide advance notice in the [XMTP Community Forums](https://community.xmtp.org/). You can also use a local network to have a client communicate with an XMTP node you are running locally. During development, it's highly recommended to use a local network environment for speed and reliability. ## Build an existing client Build, or resume, an existing client that's logged in and has an existing local database. :::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 [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 have to create a new installation the next time they log in and will lose all of their messages. ::: :::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() ``` ::: ## pages/inboxes/create-conversations.md # 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 } from "@xmtp/browser-sdk"; // response is a Map of string (identity) => boolean (is reachable) const response = await Client.canMessage([bo.identity, caro.identity]); ``` ```js [Node] import { Client } from "@xmtp/node-sdk"; // response is a Map of string (identity) => boolean (is reachable) const response = await Client.canMessage([bo.identity, caro.identity]); ``` ```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, ] ``` ::: :::tip Regarding how to handle identities that aren’t reachable, the XMTP V3.0.0 release notes will outline the next steps to ensure smooth onboarding for all participants. ::: ## Create a new group chat Once you have the verified identities, create a new group chat: :::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", ) ``` ::: ## 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) ## pages/inboxes/references.md --- description: "Access reference documentation for each XMTP client SDK" --- # XMTP SDK reference docs Use these reference docs as guides to integrating the capabilities of XMTP SDKs in your apps. - [Reference docs](https://xmtp.github.io/xmtp-react-native/modules.html) for the [React Native SDK](https://github.com/xmtp/xmtp-react-native) - [Reference docs](https://xmtp.github.io/xmtp-android/) for the [Kotlin SDK](https://github.com/xmtp/xmtp-android) ## pages/inboxes/list-stream-and-sync.mdx # List, stream, and sync conversations and messages ## List existing group chats or DMs Get a list of existing group chats or DMs in the local database. By default, the conversations are listed in descending order by their `lastMessage` created at value. If a conversation does not contain any messages, the conversation is ordered by its `createdAt` value. :::code-group ```js [Browser] const allConversations = await client.conversations.list(); const allGroups = await client.conversations.listGroups(); const allDms = await client.conversations.listDms(); ``` ```js [Node] const allConversations = await client.conversations.list(); const allGroups = await client.conversations.listGroups(); const allDms = await client.conversations.listDms(); ``` ```tsx [React Native] // List Conversation items await alix.conversations.list(); // 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 orderFilteredConversations = client.conversations.list(consentState: ALLOWED) // List just dms val conversations = alix.conversations.listDms() val orderFilteredConversations = client.conversations.listDms(consentState: ALLOWED) //List just groups val conversations = alix.conversations.listGroups() val orderFilteredConversations = client.conversations.listGroups(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 You can use a user's `isActive: true` value as a filter parameter when listing conversations. You can potentially have a separate section for "archived" or "inactive" conversations where you could use `isActive: false`. ## Stream all group chats and DMs 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](#conversationcontainer-interface). This allows the client to immediately respond to any new group chats or DMs initiated by other users. :::code-group ```js [Node] const stream = await client.conversations.stream(); // to stream only groups, use `client.conversations.streamGroups()` // to stream only dms, use `client.conversations.streamDms()` try { for await (const conversation of stream) { // Received a conversation } } catch (error) { // log any stream errors console.error(error); } ``` ```tsx [React Native] await alix.conversations.stream(async (conversation: Conversation<any>) => { // 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 all group chat and DM messages 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. :::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 [Node] // stream all messages from all conversations const stream = await client.conversations.streamAllMessages(); // stream only group messages const stream = await client.conversations.streamAllGroupMessages(); // stream only dm messages const stream = await client.conversations.streamAllDmMessages(); try { for await (const message of stream) { // Received a message } } catch (error) { // log any stream errors console.error(error); } ``` ```tsx [React Native] await alix.conversations.streamAllMessages( async (message: DecodedMessage<any>) => { // Received a message } ); ``` ```kotlin [Kotlin] alix.conversations.streamAllMessages(type: /* OPTIONAL DMS, GROUPS, ALL */).collect { // Received a message } ``` ```swift [Swift] for await message in try await alix.conversations.streamAllMessages(type: /* OPTIONAL .dms, .groups, .all */) { // Received a message } ``` ::: ## Sync ### 🎥 walkthrough: Syncing This video provides a walkthrough of syncing, covering the key ideas discussed in this doc. After watching, feel free to continue reading for more details. <iframe width="560" height="315" src="https://www.youtube.com/embed/jl7P0onApxw?si=YNafIHebx9Kxycos" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> :::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. ::: ### Sync 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 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 conversations, messages, and updates Sync all new group chat and DM conversations, messages, and installation-related updates (consent states, HMAC keys, etc.) from the network. We recommend that you run this sync in the background and call it more frequently than not. :::code-group ```js [Browser] await client.conversations.syncAll(); ``` ```js [Node] await client.conversations.syncAll(); ``` ```tsx [React Native] await client.conversations.syncAllConversations(); ``` ```kotlin [Kotlin] client.conversations.syncAllConversations() ``` ```swift [Swift] try await client.conversations.syncAllConversations() ``` ::: ## Handle unsupported content types As more [custom](/inboxes/content-types/content-types#create-a-custom-content-type) and [standards-track](/inboxes/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](/inboxes/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 } ``` ::: ## pages/inboxes/history-sync.mdx # Enable history sync for apps built with XMTP 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. <iframe width="560" height="315" src="https://www.youtube.com/embed/_FtNqtjk-ls?si=uZ_Mnh9phF6y2h-O" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> ## Enable history sync History sync is enabled by default and runs automatically. When [creating a client](/inboxes/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. ## 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/history-sync-step1.png" width="600px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/history-sync-step2.png" width="600px" /> </div> 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](#sync-messages) 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. ## pages/inboxes/pick-an-sdk.md # Pick an SDK Choose the appropriate XMTP SDK based on your app platform and development needs. ## Web This SDK is designed for web-based apps, offering seamless integration with browser environments: [Browser SDK](https://github.com/xmtp/xmtp-js/tree/main/sdks/browser-sdk) ## Server This SDK is designed for backend applications, offering seamless integration with server environments: [Node SDK](https://github.com/xmtp/xmtp-js/tree/main/sdks/node-sdk) ## Mobile These SDKs provide native support for mobile app development across different platforms and frameworks. - [React Native SDK](https://github.com/xmtp/xmtp-react-native) - [Kotlin SDK](https://github.com/xmtp/xmtp-android) - [Swift SDK](https://github.com/xmtp/xmtp-ios) ## pages/inboxes/send-messages.md # Send messages Once you have the group chat or DM conversation, you can send messages in the conversation. :::code-group ```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:  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:  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<boolean> { 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 dipslay 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](/inboxes/content-types/content-types#create-a-custom-content-type) and [standards-track](/inboxes/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](/inboxes/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 } ``` ::: ## 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 ```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 [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 [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. ## pages/inboxes/manage-inboxes.mdx # Manage XMTP inboxes and installations With XMTP, a user can have one or more inboxes they use to access their messages. 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 ID associated with the identity. To do this, you [create a client](/inboxes/create-a-client) for their identity. 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/create-inbox-install.png" width="250px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/add-installation.png" width="250px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/add-id.png" width="350px" /> </div> You can enable a user to remove an identity from their inbox. You cannot remove the recovery identity. ## 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 ```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 ```jsx [React Native] await client.removeAccount(recoveryIdentity, identityToRemove) ``` ```kotlin [Kotlin] client.removeAccount(recoveryIdentity, identityToRemove) ``` ```swift [Swift] try await client.removeAccount(recoveryIdentity: recoveryIdentity, identityToRemove: identityToRemove) ``` ::: ## 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. <iframe width="560" height="315" src="https://www.youtube.com/embed/MIw9x1Z4WXw?si=4bcbfkHTDvsM0uDG" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> ### Revoke all other installations You can revoke all installations other than the currently accessed installation. For example, consider a user using this current installation: <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/current-install-id.png" width="350px" /> </div> When the user revokes all other installations, the action removes their identity's access to all installations other than the current installation: <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/revoke-install-id.png" width="350px" /> </div> :::code-group ```jsx [React Native] await client.revokeAllOtherInstallations(recoveryIdentity) ``` ```kotlin [Kotlin] client.revokeAllOtherInstallations(recoveryIdentity) ``` ```swift [Swift] try await client.revokeAllOtherInstallations(signingKey: recoveryIdentity) ``` ::: ## View the inbox state Find an `inboxId` for an identity: :::code-group ```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 ```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" } ``` ## FAQ ### What happens when a user removes an identity? Consider an inbox with three associated identities: <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/three-ids.png" width="650px" /> </div> If the user removes an identity from the inbox, the identity no longer has access to the inbox it was removed from. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/removed-id.png" width="650px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/new-inbox-id.png" width="650px" /> </div> ### 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/two-inboxes.png" width="350px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/two-inbox-remap-id.png" width="350px" /> </div> 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 257 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/inbox1-id1.png" width="150px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/inbox1-id1-id2.png" width="350px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/inbox1-id1-inbox2-id2.png" width="350px" /> </div> 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/inbox1-id1-id2-inverse.png" width="350px" /> </div> 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). ## pages/inboxes/debug-your-app.md # Debug your inbox app You can use the XMTP Debug tool to stress and burn-in test your inbox 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). ## pages/inboxes/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](/inboxes/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 <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/group-perm-toggles.png" width="300px" /> </div> 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 :::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(); ``` ::: ## pages/inboxes/use-signatures.md # 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 ) ``` ::: ## pages/inboxes/create-a-signer.md # 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", getIdentity: () => 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", getIdentity: () => 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 ) } } ``` ::: ## pages/inboxes/content-types/custom.md --- 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](/inboxes/content-types/content-types#standard-content-types) or [standards-track](/inboxes/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](/inboxes/content-types/content-types#standard-content-types) or [standards-track](/inboxes/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](/intro/xips). ## pages/inboxes/content-types/content-types.md # 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. :::code-group ```jsx [Browser] await conversation.send("gm"); ``` ```jsx [React Native] await conversation.send("gm"); ``` ```kotlin [Kotlin] conversation.send(text = "gm") ``` ```swift [Swift] try await conversation.send(content: "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](/inboxes/content-types/attachments/#support-attachments-smaller-than-1mb): Use to send attachments smaller than 1MB. - [Remote attachment content type](/inboxes/content-types/attachments#support-remote-attachments-of-any-size): Use to send attachments of any size. - [Multiple remote attachments content type](/inboxes/content-types/attachments#support-multiple-remote-attachments-of-any-size): Use to send attachments of any size. - [Read receipt content type](/inboxes/content-types/read-receipts): Use to send a read receipt, which is a `timestamp` that indicates when a message was read. - [Reaction content type](/inboxes/content-types/reactions): Use a reaction to send a quick and often emoji-based way to respond to a message. - [Reply content type](/inboxes/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. - [On-chain transaction reference content type](/inboxes/content-types/transaction-refs): Use to send references to on-chain transactions, such as crypto payments. ## Create a custom content type 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. To learn more, see [Build custom content types](/inboxes/content-types/custom). ## pages/inboxes/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-in-a-message) 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 in a message](#support-multiple-remote-attachments-of-any-size-in-a-message). ### 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: ```bash npm i @xmtp/content-type-remote-attachment ``` ### Configure the content type After importing the package, you can register the codec. :::code-group <div data-title="Browser"> ```jsx import { ContentTypeAttachment, AttachmentCodec, RemoteAttachmentCodec, ContentTypeRemoteAttachment, } from "@xmtp/content-type-remote-attachment"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(new AttachmentCodec()); xmtp.registerCodec(new RemoteAttachmentCodec()); ``` </div> ```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 <div data-title="Browser"> 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. <p>Now that you have a `url`, you can create a `RemoteAttachment`:</p> ```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, }); ``` </div> <div data-title="React Native"> 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, }, }); ``` </div> <div data-title="Kotlin"> 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), ) ``` </div> <div data-title="Swift"> 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" ) ) ``` </div> ::: ### 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 <div data-title="Browser"> ```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; ``` </div> <div data-title="React Native"> 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 <Image source={{ uri: attached.fileUri }} /> ``` </div> ```kotlin [Kotlin] val message = newConversation.messages().first() val loadedRemoteAttachment: RemoteAttachment = messages.content() loadedRemoteAttachment.fetcher = Fetcher() runBlocking { val attachment: Attachment = loadedRemoteAttachment.load() } ``` <div data-title="Swift"> ```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)) } } ``` </div> ::: To handle unsupported content types, refer to the [fallback](/inboxes/list-stream-and-sync/#handle-unsupported-content-types) 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<RemoteAttachmentInfo> = 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<Attachment> = 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] <Image source={{ uri: attachment1.fileUri }} /> <Image source={{ uri: attachment2.fileUri }} /> ``` ```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](/inboxes/content-types/attachments) instead since many attachments are larger than 1MB in a messaging app. ::: ### Install the package ```bash npm i @xmtp/content-type-remote-attachment ``` > In some SDKs, the `AttachmentCodec` is already included in the SDK. If not, you can install the package using the following command: ### Import and register :::code-group ```jsx [Browser] import { ContentTypeAttachment, AttachmentCodec, } from "@xmtp/content-type-remote-attachment"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(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); } ``` ## pages/inboxes/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 messaging 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 ```bash [Bash] npm i @xmtp/content-type-reaction ``` > In some SDKs, the `ReactionCodec` is already included in the SDK. If not, you can install the package using the following command: ## Configure the content type After importing the package, you can register the codec. :::code-group <div data-title="Browser"> ```jsx import { ContentTypeReaction, ReactionCodec, } from "@xmtp/content-type-reaction"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(new ReactionCodec()); ``` </div> ```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](/inboxes/list-stream-and-sync/#handle-unsupported-content-types) 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. ## pages/inboxes/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: ```bash npm i @xmtp/content-type-reply ``` ## Configure the content type After importing the package, you can register the codec. :::code-group ```jsx [Browser] import { ContentTypeReply, ReplyCodec } from "@xmtp/content-type-reply"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(new ReplyCodec()); ``` ```jsx [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 ```tsx [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, }); ``` ```jsx [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 ```tsx [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](/inboxes/list-stream-and-sync/#handle-unsupported-content-types) 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). ## pages/inboxes/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 ```bash [Bash] npm i @xmtp/content-type-read-receipt ``` ## Configure the content type :::code-group <div data-title="Browser"> ```tsx import { ContentTypeReadReceipt, ReadReceiptCodec, } from "@xmtp/content-type-read-receipt"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(new ReadReceiptCodec()); ``` </div> ```jsx [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 ```jsx [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 ```tsx [Browser] if (message.contentType.sameAs(ContentTypeReadReceipt)) { // The message is a read receipt const timestamp = message.sent; } ``` ```jsx [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](/inboxes/list-stream-and-sync/#handle-unsupported-content-types) 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. ## pages/inboxes/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 on-chain transaction references. It is a reference to an on-chain transaction sent as a message. This content type facilitates sharing transaction hashes or IDs, thereby providing a direct link to on-chain activities. Transaction references serve to display transaction details, facilitating the sharing of on-chain activities, such as token transfers, between users. :::tip[Open for feedback] You're welcome to provide feedback by commenting on [XIP-21: On-chain transaction reference content type](https://community.xmtp.org/t/xip-21-on-chain-transaction-reference-content-type/532). ::: ## Install the package ```bash [Bash] npm i @xmtp/content-type-transaction-reference ``` ## Configure the content type After importing the package, you can register the codec. ```jsx [Browser] import { ContentTypeTransactionReference, TransactionReferenceCodec, } from "@xmtp/content-type-transaction-reference"; // Create the XMTP client const xmtp = await Client.create(signer, { env: "dev" }); xmtp.registerCodec(new TransactionReferenceCodec()); ``` ## Send a transaction reference With XMTP, a transaction reference is represented as an object with the following keys: ```tsx [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" }; }; ``` Once you have a transaction reference, you can send it as part of your conversation: ```jsx [Browser] await conversation.messages.send(transactionReference, { 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](/inboxes/list-stream-and-sync/#handle-unsupported-content-types) section. ```tsx [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 ``` ## 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 on-chain data before showing it to the user. ## pages/inboxes/push-notifs/pn-server.md # 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 ```  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 ```  ```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.  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).  ## 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" } ``` ::: ## pages/inboxes/push-notifs/ios-pn.md # 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. ## pages/inboxes/push-notifs/push-notifs.md # 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 ``` ::: ## 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) ``` ::: ## Resubscribe to topics to get new HMAC keys As soon as your apps receive a user preference update event 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. ## 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](#understand-apple-entitlements-for-ios-apps) (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](/inboxes/push-notifs/understand-push-notifs). Then you can: - [Set up a Go push notification server](/inboxes/push-notifs/pn-server) - [Try push notifications with the Android example XMTP app](/inboxes/push-notifs/android-pn) - [Try push notifications with the iOS example XMTP app](/inboxes/push-notifs/ios-pn) ## pages/inboxes/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.  ## 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.  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/inboxes/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](/inboxes/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](/inboxes/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](/inboxes/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](/inboxes/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. The HMAC key is derived from a generated root HMAC key, the message’s group ID, and the number of 30-day periods since the Unix epoch, along with some salt. Anytime a user gets a new installation, they get new HMAC keys for it. In this case, how do the user’s older installations learn about the user’s new installation HMAC key so they can properly decrypt and route certain messages and push notifications for that newly added installation? This is one of the jobs of the [history sync](/inboxes/history-sync) feature. It listens for `preferences.streamAllPreferenceUpdates()`, which are user preferences that may include an enum with HMAC keys for new installations. When a user’s new installation publishes updated HMAC info, older installations can see that update and must [resubscribe to topics](/inboxes/push-notifs/push-notifs#resubscribe-to-topics-to-get-new-hmac-keys) to get the new HMAC keys. ## 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. <iframe width="560" height="315" src="https://www.youtube.com/embed/YF5m_mTY6mw?si=EH0S4eP0GPEAafIw" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> 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/inboxes/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:  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  ### 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](/inboxes/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. ## pages/inboxes/push-notifs/android-pn.md # 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) ``` ## pages/inboxes/user-consent/support-user-consent.md # Support user consent preferences to provide spam-free inboxes Use the following methods to provide users with control over their messaging experience, ensuring their inboxes are tailored to their preferences and spam-free. ## Get new consent records from the network Get the latest consent records from the network: :::tip[Note] Consent syncing is backed by XMTP's history system. Syncing works only if a [history sync URL](/inboxes/create-a-client/#configure-an-xmtp-client) is specified on client create. By default, the URL to an Ephemera-hosted history sync server is set. ::: :::code-group ```tsx [React Native] await client.conversations.syncAllConversations() ``` ```kotlin [Kotlin] client.conversations.syncAllConversations() ``` ```swift [Swift] try await client.conversations.syncAllConversations() ``` ::: ## 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 records in real-time Listen for real-time updates to consent records: :::tip[Note] Consent syncing is backed by XMTP's history system. Syncing works only if a [history sync URL](/inboxes/create-a-client/#configure-an-xmtp-client) is specified on client create. By default, the URL to an Ephemera-hosted history sync server is set. ::: :::code-group ```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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/you-might-know-tab.jpg" width="400" /> </div> ### 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. <div> <img src="https://raw.githubusercontent.com/xmtp/docs-xmtp-org/refs/heads/main/docs/pages/img/hidden-requests-tab.jpg" width="400" /> </div> 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 inboxes safe? Open an [issue](https://github.com/xmtp/docs-xmtp-org/issues) to share information about it. ::: ## pages/inboxes/user-consent/user-consent.mdx # Understand how user consent preferences support spam-free inboxes 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 inboxes 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. <iframe width="560" height="315" src="https://www.youtube.com/embed/Qy_naNXYxmU?si=JEVL8Mruy_4Px4D5" title="YouTube video player" frameBorder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerPolicy="strict-origin-when-cross-origin" allowFullScreen></iframe> ## 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`. ## pages/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 | Encapsulate | lb.validator.xmtp.testnet.encapsulate.xyz:443 | | 3 | Ethereum Name Service (ENS) | grpc.ens-xmtp.com:443 | | 4 | Laminated Labs | xmtp.validators.laminatedlabs.net:443 | | 5 | Next.id | xmtp.nextnext.id:443 | | 6 | Nodle | xmtpd.nodleprotocol.io:443 | | 7 | Ephemera | grpc.testnet.xmtp.network:443 | | 8 | Ephemera | grpc2.testnet.xmtp.network:443 | Here is a map of node locations: <div style={{ position: "relative", paddingBottom: "56.25%", height: 0, overflow: "hidden" }}> <iframe src="https://www.google.com/maps/d/embed?mid=18y4nFTXQKdgiSJCQbEGl5dojPoHL3LQ&ehbc=2E312F&noprof=1" style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", border: 0 }} allowFullScreen="" loading="lazy" ></iframe> </div> ## pages/network/run-a-node.mdx # Run an XMTP network node A testnet of the decentralized XMTP network 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/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/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. # Summary This documentation contains 41 files.