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.
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 .
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.
// 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 hooksconst response = await client.getAppSettings();console.log("Current event hooks:", response.event_hooks);// STEP 2: Add pending message hook while preserving existing hooksconst 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 hooksawait client.updateAppSettings({ event_hooks: [...existingHooks, newPendingMessageHook],});
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 hooksresponse = 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 hooksnew_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 hooksclient.update_app( event_hooks=existing_hooks + [new_pending_message_hook])
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 hooksresponse = client.common.get_appexisting_hooks = response.app.event_hooks || []puts "Current event hooks:", existing_hooks# STEP 2: Add pending message hook while preserving existing hooksnew_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 hooksclient.common.update_app(Models::UpdateAppRequest.new( event_hooks: existing_hooks + [new_pending_message_hook]))
// 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));
// 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 hookssettings, err := client.GetApp(ctx, &getstream.GetAppRequest{})if err != nil { log.Fatal(err)}existingHooks := settings.Data.App.EventHooksfmt.Printf("Current event hooks: %+v\n", existingHooks)// STEP 2: Add pending message hook while preserving existing hooksnewPendingMessageHook := 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 hooksallHooks := append(existingHooks, newPendingMessageHook)_, err = client.UpdateApp(ctx, &getstream.UpdateAppRequest{ EventHooks: allHooks,})if err != nil { log.Fatal(err)}
// 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 hooksvar 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 hooksvar 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 hooksvar allHooks = new ArrayList<>(existingHooks);allHooks.add(newPendingMessageHook);client.updateApp(UpdateAppRequest.builder().eventHooks(allHooks).build()).execute();
// 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 hooksvar 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 hooksvar 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 hooksvar allHooks = new List<EventHook>(existingHooks) { newPendingMessageHook };await client.UpdateAppAsync(new UpdateAppRequest{ EventHooks = allHooks,});
See the Webhooks documentation for complete details.
For example, if your callback server url is 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:
{ "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" */ }}
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.
A user can retrieve their own pending messages using the following endpoints:
// To retrieve single messageclient.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 }}
// To retrieve single messageconst response = await client.getMessage("pending_message_id");console.log(response.message, response.pending_message_metadata);// To retrieve multiple messagesconst response = await channel.getMessagesById([ "pending_message_id_1", "pending_message_id_2",]);console.log(response.messages);
// To query single pending messagechat.getMessage(pendingMessageId, GetMessageRequest.builder().build()).execute();// To query multiple pending messageschat.channel("channel_type", "channel_id") .getManyMessages(GetManyMessagesRequest.builder() .Ids(List.of("pendingMessageId1", "pendingMessageId2")) .build());
// To retrieve single message$response = $client->getMessage("message-id", false);// To retrieve multiple messages$response = $client->getManyMessages("messaging", "general", ["message-1", "message-2"]);
require 'getstream_ruby'Models = GetStream::Generated::Models# To retrieve single messageresponse = client.chat.get_message(msg_id)# To retrieve multiple messagesresponse = client.chat.get_many_messages('messaging', channel_id, ['message-1', 'message-2'])
# To retrieve single messageclient.chat.get_message(id=msg_id)# To retrieve multiple messageschannel = client.chat.channel("messaging", "channel-id")channel.get_many_messages(ids=["message-1", "message-2"])
// To retrieve single messagemessageResp, err := client.Chat().GetMessage(ctx, "message-id", &getstream.GetMessageRequest{})// To retrieve multiple messageschannel := client.Chat().Channel("messaging", "channel-id")getMsgResp, err := channel.GetManyMessages(ctx, &getstream.GetManyMessagesRequest{ Ids: []string{"message-1", "message-2"},})
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.
// Querying multiple channelsclient.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 channelval 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 }}
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.
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.