Sending Messages
Opening a thread
createThread creates the thread on-chain and sends the first message in one transaction. The thread ID is random by default.
const { receipt, client: thread } = await client.createThread({
to: recipientPublicKey,
messageType: MessageType.Text,
content: "Hey, let's talk.",
});
CreateThreadParams
| Field | Type | Required | Description |
|---|---|---|---|
to | PublicKey | Yes | Recipient wallet address |
messageType | MessageType | Yes | Type of content |
content | string | Yes | Message body |
threadId | number | No | Random u32 if omitted |
targetInbox | Inbox | InboxClient | No | Recipient's inbox (load it first to apply payment rules correctly) |
payment | SendMsgPaymentParams | DisabledPayment | No | Payment override |
skipInboxArchivalIx | boolean | No | Skip inbox archival instruction |
Passing targetInbox is important when the recipient has a payment-gated inbox. Without it, the thread creation won't update the inbox index or validate the payment rule. Load their inbox first:
const recipientInbox = await client.inbox(recipientInboxAddress);
const { client: thread } = await client.createThread({
to: recipientPublicKey,
targetInbox: recipientInbox.Inbox,
messageType: MessageType.Text,
content: "Hello!",
// payment is auto-filled from the inbox's payment rule
});
You can also use inbox.createThread(...) if you already have the InboxClient — it fills to and targetInbox automatically.
Sending a message
After a thread exists, use sendMessage:
const { receipt, client: message } = await thread.sendMessage({
messageType: MessageType.Text,
content: "Still there?",
});
| Field | Type | Required | Description |
|---|---|---|---|
messageType | MessageType | Yes | Content type |
content | string | Yes | Message body |
msgSeq | number | No | Auto-incremented from last seq if omitted |
payment | SendMsgPaymentParams | No | Attach a token payment to this message |
skipInboxArchivalIx | boolean | No | Skip inbox archival instruction |
Message types
enum MessageType {
Text = 0, // inline UTF-8 string (stored on-chain)
Url = 1, // arbitrary URL, fetched by the reader
Ipfs = 2, // IPFS CID URL
Irys = 3, // Irys (Arweave) URL
Arweave = 4, // Arweave URL
}
For short messages, use Text — the content is stored directly in the compressed account. For longer payloads (images, encrypted blobs, long text), upload the data off-chain and store the URL using Url, Ipfs, Irys, or Arweave. The MessageClient.loadContent() method handles fetching transparently for all types.
Payments
You can attach a token transfer to any message. This is separate from the inbox payment rule — it's a one-off payment sent alongside a specific message.
await thread.sendMessage({
messageType: MessageType.Text,
content: "Here's payment for the invoice.",
payment: {
amount: new BN(1_000_000), // in token lamports
mint: usdcMint,
},
});
SendMsgPaymentParams
| Field | Type | Required | Description |
|---|---|---|---|
amount | BN | Yes | Token amount |
mint | PublicKey | Yes | Token mint address |
fromTokenAccount | PublicKey | No | Sender's token account (auto-derived as ATA if omitted) |
to | { type: "ata"; owner?: PublicKey } | { type: "raw"; address: PublicKey } | No | Recipient token account (defaults to the recipient's ATA) |
tokenProgram | PublicKey | No | Auto-detected from the mint if omitted |
Disabling payment (for payment-gated inboxes)
If you're opening a thread to a payment-gated inbox but want to explicitly skip paying — the transaction will still fail on-chain if the rule is enforced, but you can signal intent with:
payment: { disable: true }
This is useful for inboxes that allow free threads from specific senders via program-level allowlists.
Reading messages back
After sending, receipt.client is the loaded MessageClient for the message you just sent. To read the thread's history, see loading messages.