# Pending Messages

Pending Messages features lets you introduce asynchronous moderation on messages being sent on channel. To use this feature please get in touch with support so that we can enable it for your organisation.

## Sending Pending Messages

Messages can be made pending by default by setting the channel config property `mark_messages_pending` to true.

<Tabs>

```js label="Node"
response = await client.updateChannelType("messaging", {
  mark_messages_pending: true,
});
```

```python label="Python"
response = client.chat.update_channel_type(
    "messaging",
    automod="disabled",
    automod_behavior="flag",
    max_message_length=5000,
    mark_messages_pending=True,
)
```

```php label="PHP"
$client->updateChannelType("messaging", new Models\UpdateChannelTypeRequest(markMessagesPending: true));
```

```go label="Go"
_, err := client.Chat().UpdateChannelType(ctx, "messaging", &getstream.UpdateChannelTypeRequest{
	Automod:             "disabled",
	AutomodBehavior:     "flag",
	MaxMessageLength:    5000,
	MarkMessagesPending: getstream.PtrTo(true),
})
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

client.chat.update_channel_type('messaging', Models::UpdateChannelTypeRequest.new(
  mark_messages_pending: true
))
```

```java label="Java"
chat.updateChannelType("messaging",
    UpdateChannelTypeRequest.builder()
        .automod("disabled").automodBehavior("flag").maxMessageLength(5000)
        .markMessagesPending(true)
        .build())
    .execute();
```

</Tabs>

You can also set the `pending` property on a message to mark it as pending on server side (this will override the channel configuration). **Please note that this is only server-side feature** .

<Tabs>

```js label="JavaScript"
const message = await channel.sendMessage(
  {
    text: "this is my pending message",
    user_id: "user-id",
  },
  {
    pending: true,
    pending_message_metadata: {
      my: "metadata",
    },
  },
);
```

```java label="Java"
var messageRequest = MessageRequest.builder()
    .text("Hello!")
    .userID("user-id")
    .build();
Map<String, String> metadata = new HashMap<>();
metadata.put("custom_field", "true");

chat.channel("messaging", "channel-id")
    .sendMessage(SendMessageRequest.builder()
        .message(messageRequest)
        .pending(true)
        .pendingMessageMetadata(metadata)
        .build());
```

```php label="PHP"
$client->sendMessage("messaging", "general", new Models\SendMessageRequest(
    message: new Models\MessageRequest(
        id: "unique_id_21312",
        text: "hello world",
        userID: $userId,
    ),
    pending: true,
));
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

response = client.chat.send_message('messaging', channel_id, Models::SendMessageRequest.new(
  message: Models::MessageRequest.new(
    text: 'hi',
    user_id: random_user['id']
  ),
  pending: true,
  pending_message_metadata: { 'metadata' => 'some_data' }
))
```

```python label="Python"
from getstream.models import MessageRequest

response = channel.send_message(
    MessageRequest(text="hi", user_id=random_user["id"]),
    pending=True,
    pending_message_metadata={"extra_data": "test"},
)
```

```go label="Go"
channel := client.Chat().Channel("messaging", "channel-id")

messageResp, err := channel.SendMessage(ctx, &getstream.SendMessageRequest{
	Message: getstream.MessageRequest{
		Text:   getstream.PtrTo("test pending message"),
		UserID: getstream.PtrTo(userID),
	},
	Pending:                getstream.PtrTo(true),
	PendingMessageMetadata: map[string]string{"my": "metadata"},
})
```

</Tabs>

Pending messages will only be visible to the user that sent them. They will not be query-able by other users.

## Callbacks

When a pending message is either sent or deleted, the message and its associated pending message metadata are forwarded to your configured callback endpoint via HTTP(s). You may set up to two pending message hooks per application. Only the first commit to a pending message will succeed; any subsequent commit attempts will return an error, as the message is no longer pending. If multiple hooks specify a `timeout_ms`, the system will use the longest timeout value.

You can configure this callback using the dashboard or server-side SDKs.

### Using the Dashboard

1. Go to the [Stream Dashboard](https://getstream.io/dashboard/)
2. Select your app
3. Navigate to your app's settings until "Webhook & Event Configuration" section
4. Click on "Add Integration"
5. Add and configure pending message hook

![](@chat/_default/_assets/images/pending_message_dashboard.png)

### Using Server-Side SDKs

<Tabs>

```js label="JavaScript"
// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
const response = await client.getAppSettings();
console.log("Current event hooks:", response.event_hooks);

// STEP 2: Add pending message hook while preserving existing hooks
const existingHooks = response.event_hooks || [];
const newPendingMessageHook = {
  enabled: true,
  hook_type: "pending_message",
  webhook_url: "https://example.com/pending-messages",
  timeout_ms: 10000, // how long messages should stay pending before being deleted
  callback: {
    mode: "CALLBACK_MODE_REST",
  },
};

// STEP 3: Update with complete array including existing hooks
await client.updateAppSettings({
  event_hooks: [...existingHooks, newPendingMessageHook],
});
```

```python label="Python"
from getstream.models import EventHook, AsyncModerationCallbackConfig

# Note: Any previously existing hooks not included in event_hooks array will be deleted.
# Get current settings first to preserve your existing configuration.

# STEP 1: Get current app settings to preserve existing hooks
response = client.get_app()
existing_hooks = response.data.app.event_hooks or []
print("Current event hooks:", existing_hooks)

# STEP 2: Add pending message hook while preserving existing hooks
new_pending_message_hook = EventHook(
    enabled=True,
    hook_type="pending_message",
    webhook_url="https://example.com/pending-messages",
    timeout_ms=10000,  # how long messages should stay pending before being deleted
    callback=AsyncModerationCallbackConfig(mode="CALLBACK_MODE_REST"),
)

# STEP 3: Update with complete array including existing hooks
client.update_app(
    event_hooks=existing_hooks + [new_pending_message_hook]
)
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

# Note: Any previously existing hooks not included in event_hooks array will be deleted.
# Get current settings first to preserve your existing configuration.

# STEP 1: Get current app settings to preserve existing hooks
response = client.common.get_app
existing_hooks = response.app.event_hooks || []
puts "Current event hooks:", existing_hooks

# STEP 2: Add pending message hook while preserving existing hooks
new_pending_message_hook = Models::EventHook.new(
  enabled: true,
  hook_type: 'pending_message',
  webhook_url: 'https://example.com/pending-messages',
  timeout_ms: 10000, # how long messages should stay pending before being deleted
  callback: Models::AsyncModerationCallbackConfig.new(mode: 'CALLBACK_MODE_REST')
)

# STEP 3: Update with complete array including existing hooks
client.common.update_app(Models::UpdateAppRequest.new(
  event_hooks: existing_hooks + [new_pending_message_hook]
))
```

```php label="PHP"
// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
$response = $client->getApp();
$existingHooks = $response->getData()->app->eventHooks ?? [];

// STEP 2: Add pending message hook while preserving existing hooks
$newPendingMessageHook = new Models\EventHook(
    enabled: true,
    hookType: "pending_message",
    webhookUrl: "https://example.com/pending-messages",
    timeoutMs: 10000, // how long messages should stay pending before being deleted
    callback: new Models\AsyncModerationCallbackConfig(mode: "CALLBACK_MODE_REST"),
);

// STEP 3: Update with complete array including existing hooks
$allHooks = array_merge($existingHooks, [$newPendingMessageHook]);
$client->updateApp(new Models\UpdateAppRequest(eventHooks: $allHooks));
```

```go label="Go"
// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
settings, err := client.GetApp(ctx, &getstream.GetAppRequest{})
if err != nil {
	log.Fatal(err)
}
existingHooks := settings.Data.App.EventHooks
fmt.Printf("Current event hooks: %+v\n", existingHooks)

// STEP 2: Add pending message hook while preserving existing hooks
newPendingMessageHook := getstream.EventHook{
	HookType:   getstream.PtrTo("pending_message"),
	Enabled:    getstream.PtrTo(true),
	WebhookUrl: getstream.PtrTo("https://example.com/pending-messages"),
	TimeoutMs:  getstream.PtrTo(10000), // how long messages should stay pending before being deleted
	Callback:   &getstream.AsyncModerationCallbackConfig{Mode: getstream.PtrTo("CALLBACK_MODE_REST")},
}

// STEP 3: Update with complete array including existing hooks
allHooks := append(existingHooks, newPendingMessageHook)
_, err = client.UpdateApp(ctx, &getstream.UpdateAppRequest{
	EventHooks: allHooks,
})
if err != nil {
	log.Fatal(err)
}
```

```java label="Java"
// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
var response = client.getApp(GetAppRequest.builder().build()).execute().getData();
var existingHooks = response.getApp().getEventHooks();
System.out.println("Current event hooks: " + existingHooks);

// STEP 2: Add pending message hook while preserving existing hooks
var newPendingMessageHook = EventHook.builder()
    .hookType("pending_message")
    .enabled(true)
    .webhookUrl("https://example.com/pending-messages")
    .timeoutMs(10000) // how long messages should stay pending before being deleted
    .callback(AsyncModerationCallbackConfig.builder().mode("CALLBACK_MODE_REST").build())
    .build();

// STEP 3: Update with complete array including existing hooks
var allHooks = new ArrayList<>(existingHooks);
allHooks.add(newPendingMessageHook);
client.updateApp(UpdateAppRequest.builder().eventHooks(allHooks).build()).execute();
```

```csharp label="C#"
// Note: Any previously existing hooks not included in event_hooks array will be deleted.
// Get current settings first to preserve your existing configuration.

// STEP 1: Get current app settings to preserve existing hooks
var settings = await client.GetAppAsync();
var existingHooks = settings.App.EventHooks ?? new List<EventHook>();
Console.WriteLine($"Current event hooks: {existingHooks}");

// STEP 2: Add pending message hook while preserving existing hooks
var newPendingMessageHook = new EventHook
{
    HookType = "pending_message",
    Enabled = true,
    WebhookUrl = "https://example.com/pending-messages",
    TimeoutMs = 10000, // how long messages should stay pending before being deleted
    Callback = new AsyncModerationCallbackConfig { Mode = "CALLBACK_MODE_REST" },
};

// STEP 3: Update with complete array including existing hooks
var allHooks = new List<EventHook>(existingHooks) { newPendingMessageHook };
await client.UpdateAppAsync(new UpdateAppRequest
{
    EventHooks = allHooks,
});
```

</Tabs>

See the [Webhooks](/chat/docs/react/webhooks-overview/) documentation for complete details.

### Callback Request

For example, if your callback server url is [https://example.com](https://example.com), we would send callbacks:

- When pending message is sent

`POST https://example.com/PassOnPendingMessage`

- When a pending message is deleted

`POST https://https://example.com/DeletedPendingMessage`

In both callbacks, the body of the POST request will be of the form:

<Tabs>

```json label="JSON"
{
  "message": {
    // the message object
  },
  "metadata": {
    // keys and values that you passed as pending_message_metadata
  },
  "request_info": {
    // request info of the request that sent the pending message. Example:
    /*
    "type": "client",
    "ip": "127.0.0.1",
    "user_agent": "Mozilla/5.0...",
    "sdk": "stream-chat-js",
    "ext": "additional-data"
    */
  }
}
```

</Tabs>

## Deleting pending messages

Pending messages can be deleted using the normal delete message endpoint. Users are only able to delete their own pending messages. The messages must be hard deleted. Soft deleting a pending message will return an error.

## Updating pending messages

Pending messages cannot be updated.

## Querying pending messages

A user can retrieve their own pending messages using the following endpoints:

<Tabs>

```kotlin label="Kotlin"
// To retrieve single message
client.getPendingMessage("pending_message_id").enqueue { result ->
    if (result is Result.Success) {
        val message: Message = result.value.message
        val metadata: Map<String, String> = result.value.metadata
    }
}
```

```js label="Node"
// To retrieve single message
const response = await client.getMessage("pending_message_id");
console.log(response.message, response.pending_message_metadata);

// To retrieve multiple messages
const response = await channel.getMessagesById([
  "pending_message_id_1",
  "pending_message_id_2",
]);
console.log(response.messages);
```

```java label="Java"
// To query single pending message
chat.getMessage(pendingMessageId, GetMessageRequest.builder().build()).execute();

// To query multiple pending messages
chat.channel("channel_type", "channel_id")
    .getManyMessages(GetManyMessagesRequest.builder()
        .Ids(List.of("pendingMessageId1", "pendingMessageId2"))
        .build());
```

```php label="PHP"
// To retrieve single message
$response = $client->getMessage("message-id", false);

// To retrieve multiple messages
$response = $client->getManyMessages("messaging", "general", ["message-1", "message-2"]);
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

# To retrieve single message
response = client.chat.get_message(msg_id)

# To retrieve multiple messages
response = client.chat.get_many_messages('messaging', channel_id, ['message-1', 'message-2'])
```

```python label="Python"
# To retrieve single message
client.chat.get_message(id=msg_id)

# To retrieve multiple messages
channel = client.chat.channel("messaging", "channel-id")
channel.get_many_messages(ids=["message-1", "message-2"])
```

```go label="Go"
// To retrieve single message
messageResp, err := client.Chat().GetMessage(ctx, "message-id", &getstream.GetMessageRequest{})

// To retrieve multiple messages
channel := client.Chat().Channel("messaging", "channel-id")
getMsgResp, err := channel.GetManyMessages(ctx, &getstream.GetManyMessagesRequest{
	Ids: []string{"message-1", "message-2"},
})
```

</Tabs>

## Query channels

Each channel that is returned from query channels will also have an array of `pending_messages` . These are pending messages that were sent to this channel, and belong to the user who made the query channels call. This array will contain a maximum of 100 messages and these will be the 100 most recently sent messages.

<Tabs>

```kotlin label="Kotlin"
// Querying multiple channels
client.queryChannels(request).enqueue { result ->
    if (result is Result.Success) {
        val channels: List<Channel> = result.value
        // Pending messages in the first channel
        val pendingMessages: List<PendingMessage> = channels[0].pendingMessages
    }
}

// Querying single channel
val channelClient = client.channel("messaging", "channel_id")
channelClient.query(request).enqueue { result ->
    if (result is Result.Success) {
        val channel: Channel = result.value
        // Pending messages in the channel
        val pendingMessages: List<PendingMessage> = channel.pendingMessages
    }
}
```

```js label="Node"
// Quering multiple channels
const channels = await client.queryChannels(filters);
console.log(channels[0].state.pending_messages);

// Querying single channel
const channel = await client.channel("messaging", "channel_id");
await channel.query();
console.log(channel.state.pending_messages);
```

```java label="Java"
var response = chat.queryChannels(QueryChannelsRequest.builder()
    .userID("user-id")
    .sort(List.of(SortParamRequest.builder().field("id").direction(-1).build()))
    .build()).execute().getData();

System.out.println(response.getChannels().get(0).getPendingMessages());
```

```php label="PHP"
$response = $client->queryChannels(new Models\QueryChannelsRequest(
    filterConditions: (object)["id" => $channelId],
    userID: $userId,
));
$data = $response->getData();
print_r($data->channels[0]->pendingMessages);
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

response = client.chat.query_channels(Models::QueryChannelsRequest.new(
  filter_conditions: { 'members' => { '$in' => ['legolas'] } },
  sort: [Models::SortParamRequest.new(field: 'id', direction: 1)],
  user_id: user_id
))

# Access pending messages
# response.channels[0].pending_messages
```

```go label="Go"
resp, _ := client.Chat().QueryChannels(ctx, &getstream.QueryChannelsRequest{
	FilterConditions: map[string]any{"type": "messaging"},
	UserID:           getstream.PtrTo(userID),
})

fmt.Println("Pending Messages: ", resp.Data.Channels[0].PendingMessages)
```

</Tabs>

## Committing pending messages

Calling the commit message endpoint will promote a pending message into a normal message. This message will then be visible to other users and any events/push notifications associated with the message will be sent.

The commit message endpoint is server-side only.

<Tabs>

```js label="Node"
await serverClient.commitMessage(id);
```

```java label="Java"
chat.commitMessage("pending_message_id", CommitMessageRequest.builder().build()).execute();
```

```php label="PHP"
$client->commitMessage($msgId, new Models\CommitMessageRequest());
```

```ruby label="Ruby"
require 'getstream_ruby'
Models = GetStream::Generated::Models

client.chat.commit_message('message-1', Models::CommitMessageRequest.new)
```

```python label="Python"
client.chat.commit_message(id="message-1")
```

```go label="Go"
client.Chat().CommitMessage(ctx, "message-id", &getstream.CommitMessageRequest{})
```

</Tabs>

If a message has been in the pending state longer than the `timeout_ms` defined for your app, then the pending message will be deleted. The default timeout for a pending message is 3 days.


---

This page was last updated at 2026-05-22T16:32:20.839Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/react/pending-messages/](https://getstream.io/chat/docs/react/pending-messages/).