---
source_url: https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/threads
title: Message threads
updated_at: 2026-06-01T12:01:23.203Z
---

> Documentation Index
> For a curated overview of PubNub documentation, see: https://www.pubnub.com/docs/llms.txt
> For the full list of all documentation pages, see: https://www.pubnub.com/docs/llms-full.txt


# Message threads

Organize conversations into threads for topic-specific discussions.

Benefits:

* Focus discussions on specific topics
* Clarify and resolve issues without cross-talk
* Keep main channel conversations clean

Thread channels have IDs starting with `PUBNUB_INTERNAL_THREAD_{channel_id}_{message_id}`. Messages with threads have `hasThread: true`.

[ThreadMessage](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/thread-message) and [ThreadChannel](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/thread-channel) extend the base `message` and `channel` entities with thread-specific methods.

## Interactive demo

See threads and [quoted messages](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/quotes) in a React app.

:::tip Want to implement something similar?
See [how to](https://www.pubnub.com/how-to/chat-sdk-create-threads-and-quote-messges/) or view the [source code](https://github.com/PubNubDevelopers/Chat-SDK-How-Tos/tree/main/threads-quotes).
:::

### Test it out

Click **Reset App State Globally** to clear all previously added quotes and replies (if there are any).

Reply to a message in a thread by typing in a message in the input field and pressing the arrow to send it.

## Create thread

`createThread()` creates a thread (channel) for a selected message, sends the first reply, and returns both the thread channel and the updated parent message with `hasThread: true`.

##### Under the hood

`createThread()` calls PubNub App Context and Message Actions APIs and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata) and [addMessageAction](https://www.pubnub.com/docs/sdks/javascript/api-reference/message-actions#add-message-reaction) methods.

### Method signature

This method has the following signature:

```ts
message.createThread(
    text: string,
    options?: SendTextParams
): Promise<CreateThreadResult>
```

#### Input

| Parameter | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| text | string | Yes |  | Text that you want to send as the first message in the thread. |
| options | SendTextParams | Optional |  | Object with additional options for the thread message. Refer to [sendText()](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/send-receive) for the full list of supported parameters. |

#### Output

| Type | Description |
| --- | --- |
| `Promise<CreateThreadResult>` | Object containing both the thread channel and the updated parent message. |

The returned `CreateThreadResult` object contains:

| Property | Description |
| --- | --- |
| `threadChannel`Type: `ThreadChannel` | The newly created thread channel for sending and receiving messages. |
| `parentMessage`Type: `Message` | The updated parent message with `hasThread` set to `true`. |

#### Errors

Whenever you try to create a thread for a message that is already in a thread (published on a channel with the ID starting with `MESSAGE_THREAD_ID_PREFIX`), you'll receive the `Only one level of thread nesting is allowed` error. If you try to create a thread for a message that already contains a thread, you'll get the `Thread for this message already exists` error.

### Sample code

Create a thread for the last message on the `support` channel.

```ts
// reference the channel with the message
const channel = await chat.getChannel("support")
// get the last message on this channel
const message = (await channel.getHistory({count: 1})).messages[0]
// create a thread and get the result with updated parent
const { threadChannel, parentMessage } = await message.createThread(
    "This is the first reply in the thread"
)

// parentMessage now has hasThread = true without needing to re-fetch
console.log("Thread created:", threadChannel.id)
console.log("Parent has thread:", parentMessage.hasThread) // true
```

## Create thread with result

:::warning Deprecated
This method is deprecated. Use [createThread()](#create-thread) instead, which now returns `CreateThreadResult` directly.
:::

`createThreadWithResult()` creates a thread, sends the first reply, and returns both the thread channel and updated parent message with `hasThread: true`. Use this when you need immediate confirmation without a separate fetch.

##### Under the hood

`createThreadWithResult()` calls PubNub App Context and Message Actions APIs and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata), [addMessageAction](https://www.pubnub.com/docs/sdks/javascript/api-reference/message-actions#add-message-reaction), and [publish()](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe#publish) methods.

### Method signature

This method has the following signature:

```ts
message.createThreadWithResult(
    text: string,
    options?: SendTextParams
): Promise<CreateThreadResult>
```

#### Input

| Parameter | Description |
| --- | --- |
| `text` *Type: `string`Default: n/a | Text that you want to send as the first message in the thread. |
| `options`Type: `SendTextParams`Default: n/a | Object with additional options for the thread message. Refer to [sendText()](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/send-receive) for the full list of supported parameters. |

#### Output

| Type | Description |
| --- | --- |
| `Promise<CreateThreadResult>` | Object containing both the `threadChannel` and the updated `parentMessage`. |

The returned `CreateThreadResult` object contains:

| Property | Description |
| --- | --- |
| `threadChannel`Type: `ThreadChannel` | The newly created thread channel for sending and receiving messages. |
| `parentMessage`Type: `Message` | The updated parent message with `hasThread` set to `true`. |

#### Errors

Whenever you try to create a thread for a message that is already in a thread (published on a channel with the ID starting with `MESSAGE_THREAD_ID_PREFIX`), you'll receive the `Only one level of thread nesting is allowed` error. If you try to create a thread for a message that already contains a thread, you'll get the `Thread for this message already exists` error.

### Sample code

Create a thread for the last message on the `support` channel and get the updated parent message.

```ts
// reference the channel with the message
const channel = await chat.getChannel("support")
// get the last message on this channel
const message = (await channel.getHistory({count: 1})).messages[0]
// create a thread and get the result with updated parent
const { threadChannel, parentMessage } = await message.createThreadWithResult(
    "This is the first reply in the thread"
)

// parentMessage now has hasThread = true without needing to re-fetch
console.log("Thread created:", threadChannel.id)
console.log("Parent has thread:", parentMessage.hasThread) // true
```

## Send thread message

Reply to a message in a thread by calling the `sendText()` method from the previously created `threadChannel` object.

### Method signature

Head over to the [sendText() method](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/send-receive) section for details.

### Sample code

Send a message in a thread created for the last message on the `support` channel.

```ts
// reference the channel where you want to send a message
const channel = await chat.getChannel("support")
// get the last message on the channel to which you want to reply in a thread
const message = (await channel.getHistory({count: 1})).messages[0]
// get the thread channel
const threadChannel = await message.getThread()
// send a message in the thread
threadChannel.sendText("Good job, guys!")
```

## Get thread

Get the thread channel on which the thread message is published.

##### Under the hood

`getThread()` calls PubNub App Context API and the JavaScript SDK [getChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#get-channel-metadata) method.

### Method signature

This method has the following signature:

```ts
message.getThread(): Promise<ThreadChannel>
```

#### Input

This method doesn't take any parameters.

#### Output

| Type | Description |
| --- | --- |
| `Promise<ThreadChannel>` | Object returning the thread channel metadata. |

### Sample code

Get the thread channel created from the message with the `16200000000000001` timetoken.

```ts
// reference the "incident-management" parent channel
const channel = await chat.getChannel("incident-management")
// return the message object
const message = await channel.getMessage("16200000000000001")
// get the thread channel
const threadChannel = await message.getThread()
```

## Check if message starts thread

`hasThread` indicates if a message starts a thread.

### Method signature

This method has the following signature:

```ts
message.hasThread: boolean
```

#### Properties

| Property | Description |
| --- | --- |
| `hasThread`Type: `boolean` | Info on whether the message already starts a thread or not. |

### Sample code

Check if the message with the `16200000000000001` timetoken starts a thread.

```ts
// reference the "message" that you're interested in
const message = await channel.getHistory({
    startTimetoken: "16200000000000000",
    endTimetoken: "162000000000000001",
})

// check if the message starts a thread
message.hasThread
```

## Get thread updates

`onThreadMessageUpdated()` on `ThreadMessage` receives updates when a single thread message or its [reactions](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/reactions) change.

The callback fires whenever the message is edited, deleted, or its reactions change. Returns an unsubscribe function.

### Method signature

##### Under the hood

`onThreadMessageUpdated()` calls Pub/Sub API and the JavaScript SDK [subscribe()](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe#subscribe) and [unsubscribe()](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe#unsubscribe) methods.

This method takes the following parameters:

```ts
threadMessage.onThreadMessageUpdated(callback: (message: ThreadMessage) => void): () => void
```

#### Input

| Parameter | Description |
| --- | --- |
| `callback` *Type: `(message: ThreadMessage) => void`Default: n/a | Callback function invoked whenever the thread message is updated or its reactions change. Receives the updated `ThreadMessage` object. |

#### Output

| Type | Description |
| --- | --- |
| `() => void` | Function you can call to disconnect (unsubscribe) from the channel and stop receiving updates. |

### Sample code

Listen for updates to the last message in a thread on the `support` channel.

```ts
const channel = await chat.getChannel("support")
const message = (await channel.getHistory({count: 1})).messages[0]
const threadChannel = await message.getThread()
const threadMessage = (await threadChannel.getHistory({count: 1})).messages[0]
const stopUpdates = threadMessage.onThreadMessageUpdated((updatedMessage) => {
    console.log("Thread message updated: ", updatedMessage)
})
// after some time...
stopUpdates()
```

## Get thread updates

`streamUpdatesOn()` on `ThreadMessage` receives updates when thread messages or [reactions](https://www.pubnub.com/docs/chat/chat-sdk/build/features/messages/reactions) change.

The callback fires whenever messages are added, edited, deleted, or reactions change. Returns an unsubscribe function.

:::note Stream update behavior
`streamUpdatesOn()` returns the complete list of monitored thread messages on each change.
:::

### Method signature

##### Under the hood

`streamUpdatesOn()` calls Pub/Sub API and the JavaScript SDK [subscribe()](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe#subscribe) and [unsubscribe()](https://www.pubnub.com/docs/sdks/javascript/api-reference/publish-and-subscribe#unsubscribe) methods.

This method takes the following parameters:

```ts
static ThreadMessage.streamUpdatesOn(
    threadMessages: ThreadMessage[]
    callback: (threadMessages: ThreadMessage[]) => unknown
): () => void
```

#### Input

| Parameter | Description |
| --- | --- |
| `threadMessages` *Type: `ThreadMessage[]`Default: n/a | Array of [ThreadMessage objects](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/thread-message) for which you want to get updates on changed message threads or related message reactions. |
| `callback` *Type: n/aDefault: n/a | Callback function passed to the method as a parameter. It defines the custom behavior to be executed when detecting changes in message threads or related message reactions. |
| `> threadMessages` *Type: `ThreadMessage[]`Default: n/a | Returned array of `ThreadMessage` objects with the updated message threads or related reactions. |

#### Output

| Type | Description |
| --- | --- |
| `() => void` | Function you can call to disconnect (unsubscribe) from the channel and stop receiving `objects` events. |

#### Errors

Whenever a list of `ThreadMessage` objects is required as a parameter, and you try to get updates on message threads and message reactions without specifying the list, you will receive the `Cannot stream message updates on an empty list` error.

### Sample code

Get message threads and message reaction-related updates for the first page of messages published in a thread on the `support` channel.

```ts
const channel = await chat.getChannel("support")
const { messages: threadMessages } = await channel.getHistory()
ThreadMessage.streamUpdatesOn(threadMessages, (threadMessages) => {
    // The callback receives the complete list of all thread messages you're monitoring
    // (including all reactions) each time any change occurs.
    console.log("Updated messages: ", threadMessages)
})
```

### Other examples

Stop listening to updates for the last ten messages published in a thread on the `support` channel.

```ts
const channel = await chat.getChannel("support")
const { messages: threadMessages } = await channel.getHistory()
const stopUpdates = ThreadMessage.streamUpdatesOn(threadMessages, /* handle updates callback */)
// after some time...
stopUpdates()
```

## Get historical thread message

`getHistory()` on `threadChannel` fetches historical thread messages.

##### Under the hood

`getHistory()` calls Message Persistence API and the JavaScript SDK [fetchMessages()](https://www.pubnub.com/docs/sdks/javascript/api-reference/storage-and-playback#fetch-history) method.

### Method signature

This method takes the following parameters:

```ts
threadChannel.getHistory({
    startTimetoken?: string;
    endTimetoken?: string;
    count?: number;
}): Promise<{
    messages: ThreadMessage[];
    isMore: boolean;
}>
```

#### Input

| Parameter | Description |
| --- | --- |
| `startTimetoken`Type: `string`Default: n/a | [Timetoken](https://www.pubnub.com/docs/sdks/javascript/api-reference/misc#time) delimiting the start of a time slice (exclusive) to pull thread messages from. For details, refer to the [Fetch History section](https://www.pubnub.com/docs/sdks/javascript/api-reference/storage-and-playback#fetch-history). |
| `endTimetoken`Type: `string`Default: n/a | Timetoken delimiting the end of a time slice (inclusive) to pull thread messages from. For details, refer to the [Fetch History section](https://www.pubnub.com/docs/sdks/javascript/api-reference/storage-and-playback#fetch-history). |
| `count`Type: `number`Default: `25` | Number of historical thread messages to return for the channel in a single call. Since each call returns all attached message reactions by default, the maximum number of returned thread messages is `25`. For more details, refer to the description of the `includeMessageActions` parameter in the [JavaScript SDK docs](https://www.pubnub.com/docs/sdks/javascript/api-reference/storage-and-playback#methods). |

#### Output

| Parameter | Description |
| --- | --- |
| `Promise<>`Type: `object` | Returned object containing two fields: `messages` and `isMore`. |
| `> messages`Type: `ThreadMessage[]` | Array listing the requested number of historical thread [Message objects](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/message). |
| `> isMore`Type: `boolean` | Whether there are any more historical thread messages to pull. |

By default, each call returns all message reactions and metadata attached to the retrieved thread messages.

### Sample code

From the thread created for the last message in the `support` channel, fetch `10` historical thread messages that are older than the timetoken `15343325214676133`.

```ts
// reference the "support" channel
const channel = await chat.getChannel("support")
// get the last message on the channel, which is the root message for the thread
const message = (await channel.getHistory({count: 1})).messages[0]
// get the thread channel
const threadChannel = await message.getThread()
// fetch the required historical messages
threadMessages = await threadChannel.getHistory(
    {
        startTimetoken: "15343325214676133",
        count: 10
    }
)
```

## Remove thread

`removeThread()` removes a thread (channel) for a selected message.

##### Under the hood

`removeThread()` calls PubNub App Context and Message Actions APIs and the JavaScript SDK [removeChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#remove-channel-metadata) and [removeMessageAction](https://www.pubnub.com/docs/sdks/javascript/api-reference/message-actions#remove-message-reaction) methods.

### Method signature

This method has the following signature:

```ts
message.removeThread(): Promise<boolean>
```

#### Input

This method doesn't take any parameters.

#### Output

| Type | Description |
| --- | --- |
| `Promise<boolean>` | Returns `true` when the thread is successfully removed. |

#### Errors

Whenever you try to remove a thread that doesn't exist, you'll get the `There is no thread to be deleted` error.

### Sample code

Remove a thread for the last message on the `support` channel.

```ts
// reference the channel with the message
const channel = await chat.getChannel("support")
// get the last message on this channel
const message = (await channel.getHistory({count: 1})).messages[0]
// remove the thread for this message
const result = await message.removeThread()
console.log("Thread removed:", result) // true
```

## Pin thread message to thread channel

`pinMessage()` on `ThreadChannel` pins a thread message to the thread channel.

##### Under the hood

`pinMessage()` calls PubNub App Context API and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata) method.

### Method signature

This method takes the following parameters:

```ts
threadChannel.pinMessage(
    message: ThreadMessage
): Promise<ThreadChannel>
```

#### Input

| Parameter | Description |
| --- | --- |
| `message` *Type: `ThreadMessage`Default: n/a | [ThreadMessage object](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/thread-message) you want to pin to the selected thread channel. |

#### Output

| Type | Description |
| --- | --- |
| `Promise<ThreadChannel>` | Object returning the thread channel metadata updated with these custom fields: `pinnedMessageTimetoken` to mark the timetoken when the message was pinned `pinnedMessageChannelID` to mark the channel on which the message was pinned to the thread channel (unpinning was performed either directly on the parent channel or on a thread channel). |

### Sample code

A thread was created for the last message in the `support` parent channel. Pin the last message from this thread to the thread channel.

```ts
// reference the "support" channel where the root message for the thread is published
const channel = await chat.getChannel("support")
// get the last message on the channel, which is the root message for the thread
const message = (await channel.getHistory({count: 1})).messages[0]
// get the thread channel
const threadChannel = await message.getThread()
// fetch the last message in the thread that you want to pin
const lastMessage = (await threadChannel.getHistory({count: 1})).messages[0]
// pin the message to the thread channel
const pinnedMessage = await threadChannel.pinMessage(lastMessage)
```

## Pin thread message to parent channel

`pinMessageToParentChannel()` (on `ThreadChannel`) and `pinToParentChannel()` (on `ThreadMessage`) pin a thread message to the parent channel.

##### Under the hood

`pinMessageToParentChannel()` and `pinToParentChannel()` call PubNub App Context API and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata) method.

### Method signature

These methods take the following parameters:

* pinMessageToParentChannel() 1threadChannel.pinMessageToParentChannel(2 message: ThreadMessage3): Promise<Channel>
* pinToParentChannel() 1threadMessage.pinToParentChannel(): Promise<Channel>

#### Input

| Parameter | Required in `pinMessageToParentChannel()` | Required in `pinToParentChannel()` | Description |
| --- | --- | --- | --- |
| `message`Type: `ThreadMessage`Default: n/a | Yes | No | [ThreadMessage object](https://www.pubnub.com/docs/chat/chat-sdk/learn/chat-entities/thread-message) you want to pin to the selected parent channel. |

#### Output

| Type | Description |
| --- | --- |
| `Promise<Channel>` | Object returning the channel metadata updated with these custom fields: `pinnedMessageTimetoken` to mark the timetoken when the message was pinned `pinnedMessageChannelID` to mark the channel on which the message was pinned to the parent channel (pinning was performed either directly on the parent channel or on a thread channel). |

#### Errors

Whenever you try to pin the thread message to the removed parent channel, you will receive the `Parent channel doesn't exist` error.

### Sample code

A thread was created for the last message in the `support` parent channel. Pin the last message from this thread to the parent channel.

* pinMessageToParentChannel() 1// reference the "support" channel where the root message for the thread is published2const channel = await chat.getChannel("support")3// get the last message on the channel, which is the root message for the thread4const message = (await channel.getHistory({count: 1})).messages[0]5// get the thread channel6const threadChannel = await message.getThread()7// get the last message on the thread channel8const threadMessage = (await threadChannel.getHistory({count: 1})).messages[0]9// pin the message to the parent channel10const pinnedMessage = await threadChannel.pinMessageToParentChannel(threadMessage)
* pinToParentChannel() 1// reference the "support" channel where the root message for the thread is published2const channel = await chat.getChannel("support")3// get the last message on the channel, which is the root message for the thread4const message = (await channel.getHistory({count: 1})).messages[0]5// get the thread channel6const threadChannel = await message.getThread()7// get the last message on the thread channel8const threadMessage = (await threadChannel.getHistory({count: 1})).messages[0]9// pin the message to the parent channel10const pinnedMessage = await threadMessage.pinToParentChannel()

## Unpin thread message from thread channel

`unpinMessage()` on `ThreadChannel` unpins the pinned thread message.

##### Under the hood

`unpinMessage()` calls PubNub App Context API and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata) method.

### Method signature

This method has the following signature:

```ts
threadChannel.unpinMessage(): Promise<ThreadChannel>
```

#### Input

This method doesn't take any parameters.

#### Output

| Type | Description |
| --- | --- |
| `Promise<ThreadChannel>` | Object returning the thread channel metadata updated with these custom fields: `pinnedMessageTimetoken` to mark the timetoken when the message was unpinned `pinnedMessageChannelID` to mark the channel on which the message was unpinned from the thread channel (unpinning was performed either directly on the parent channel or on a thread channel). |

### Sample code

Unpin the thread message from the thread (channel) created for the last message on the `support` channel.

```ts
// reference the "support" channel where the root message for the thread is published
const channel = await chat.getChannel("support")
// get the last message on the channel, which is the root message for the thread
const message = (await channel.getHistory({count: 1})).messages[0]
// get the thread channel
const threadChannel = await message.getThread()
// unpin the message from the thread channel
const unpinnedMessage = await threadChannel.unpinMessage()
```

## Unpin thread message from parent channel

`unpinMessageFromParentChannel()` (on `ThreadChannel`) and `unpinFromParentChannel()` (on `ThreadMessage`) unpin a thread message from the parent channel.

##### Under the hood

`unpinMessageFromParentChannel()` and `unpinFromParentChannel()` call PubNub App Context API and the JavaScript SDK [setChannelMetadata()](https://www.pubnub.com/docs/sdks/javascript/api-reference/objects#set-channel-metadata) method.

### Method signature

These methods have the following signatures:

* unpinMessageFromParentChannel() 1threadChannel.unpinMessageFromParentChannel(): Promise<Channel>
* unpinFromParentChannel() 1threadMessage.unpinFromParentChannel(): Promise<Channel>

#### Input

These methods don't take any parameters.

#### Output

| Type | Description |
| --- | --- |
| `Promise<Channel>` | Object returning the channel metadata updated with these custom fields: `pinnedMessageTimetoken` to mark the timetoken when the message was unpinned `pinnedMessageChannelID` to mark the channel on which the message was unpinned from the parent channel (unpinning was performed either directly on the parent channel or on a thread channel). |

#### Errors

Whenever you try to unpin the thread message from the removed parent channel, you will receive the `Parent channel doesn't exist` error.

### Sample code

Unpin the thread message from the `support` parent channel.

* unpinMessageFromParentChannel() 1// reference the "support" channel where the root message for the thread is published2const channel = await chat.getChannel("support")3// get the last message on the channel, which is the root message for the thread4const message = (await channel.getHistory({count: 1})).messages[0]5// get the thread channel6const threadChannel = await message.getThread()7// unpin the message from the parent channel8const unpinnedMessage = await threadChannel.unpinFromParentChannel()
* unpinFromParentChannel() 1// reference the "support" channel where the root message for the thread is published2const channel = await chat.getChannel("support")3// get the last message on the channel, which is the root message for the thread4const message = (await channel.getHistory({count: 1})).messages[0]5// get the thread channel6const threadChannel = await message.getThread()7// reference the pinned message8const threadMessage = (await threadChannel.getHistory({count: 1})).messages[0]9// unpin the message from the parent channel10const unpinnedMessage = await threadMessage.unpinFromParentChannel()