Getting Started

Chat Client

Let's get started by initializing the client and setting the current user:


const client = new StreamChat("YOUR_API_KEY");
await client.setUser(
    {
        id: 'jlahey',
        name: 'Jim Lahey',
        image: 'https://i.imgur.com/fR9Jz14.png',
    },
    "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiamxhaGV5In0.OkDbpbujWJ-XIVHaf00Dnqt3v8Yp_nQ6CGzm-Z4QUVc",
);
                    

// typically done in your BaseApplication class
StreamChat.init("qk4nn7rpcn75", new ApiClientOptions.Builder().Timeout(6666).build(), getApplicationContext());

// set the user to establish the websocket connection
// usually done when you open the chat interface
Client client = StreamChat.getInstance(getApplication());

// this hashmap allows you to add any custom fields you want to store about your user
// the UI components will pick up name and image by default
HashMap<string, object=""> extraData = new HashMap&lt;&gt;();
extraData.put("name", "Bender");
extraData.put("image", "https://bit.ly/321RmWb");
User user = new User(USER_ID, extraData);
client.setUser(user, "FEED_USER_TOKEN", new ClientConnectionCallback() {
    @Override
    public void onSuccess(User user) {
        Log.i(TAG, String.format("Connection established for user %s", user.getName()));
    }

    @Override
    public void onError(String errMsg, int errCode) {
        Log.e(TAG, String.format("Failed to establish websocket connection. Code %d message %s", errCode, errMsg));
    }
});</string,>
                    

// In your AppDelegate:
// Import Stream chat core framework.
import StreamChatCore

// Setup the Stream Chat Client with your API key 
// in func application(_ application:didFinishLaunchingWithOptions:).
Client.config = .init(apiKey: "&lt;#API_KEY#&gt;")

// Create a user, when he/she was logged in.
let user = User(id: "bender", name: "Bender", avatarURL: URL(string: "https://bit.ly/321RmWb")!)

// Setup the current user and token.
Client.shared.set(user: user, token: token)
                    

// typically done in your BaseApplication class
StreamChat.init(
    "qk4nn7rpcn75", ApiClientOptions.Builder().Timeout(6666).build(),
    applicationContext
)

// set the user to establish the websocket connection
// usually done when you open the chat interface
val client = StreamChat.getInstance(application)

// this hashmap allows you to add any custom fields you want to store about your user
// the UI components will pick up name and image by default
val extraData = HashMap<string, any="">()
extraData.put("name", "Bender")
extraData.put("image", "https://bit.ly/321RmWb")
val user = User("USER_ID", extraData)
client.setUser(user, "FEED_USER_TOKEN", object : ClientConnectionCallback {
    override fun onSuccess(user: User) {
        Log.i(TAG,String.format("Connection established for user %s", user.name) )
    }

    override fun onError(errMsg: String, errCode: Int) {
        Log.e(TAG,
            String.format("Failed to establish websocket connection. Code %d message %s",
                errCode,
                errMsg
            )
        )
    }
})</string,>
                    

The above snippet is for an in-browser or mobile integration. Server-side API calls are a little different, but this is covered in detail later in the documentation.

Channels

Let’s continue by initializing your first channel. A channel contains messages, a list of people that are watching the channel, and optionally a list of members (for private conversations). The example below shows how to set up a channel to support chat for a group conversation:


const channel = client.channel('messaging', 'travel', {
    name: 'Awesome channel about traveling',
});
// fetch the channel state, subscribe to future updates
let state = await channel.watch();
                    

// initialize a channel object with an extra data hashmap
HashMap<string, object=""> extraData = new HashMap&lt;&gt;();
extraData.put("name", "Talking about life");

Channel channel = client.channel(channelType, channelID, extraData);

// watching a channel's state
ChannelQueryRequest request = new ChannelQueryRequest().
          withMessages(Constant.DEFAULT_LIMIT).withWatch();

// note how the withWatch() argument ensures that we are watching the channel for any changes/new messages
channel.query(
  request,
  new QueryChannelCallback() {
      @Override
      public void onSuccess(ChannelState response) {          
      }

      @Override
      public void onError(String errMsg, int errCode) {          
      }
  }
);</string,>
                    

// Create an extra data for a channel.
struct ChannelInfo: Codable {
    let info: String
}

// Register once your extra data types for the decoding.
ExtraData.decodableTypes = [.channel(ChannelInfo.self)]

let info = ChannelInfo(info: "Awesome channel about traveling")
let channel = Channel(type: .messaging, id: "travel", name: "Travel", extraData: info)
                    

// initialize a channel object with an extra data hashmap
val extraData = HashMap<string, any="">()
extraData.put("name", "Talking about life")

val channel = client.channel(channelType, channelID, extraData)

// watching a channel's state
val request = ChannelQueryRequest().withMessages(Constant.DEFAULT_LIMIT).withWatch()

// note how the withWatch() argument ensures that we are watching the channel for any changes/new messages
channel.query(
    request,
    object : QueryChannelCallback {
        override fun onSuccess(response: ChannelState) {
        }

        override fun onError(errMsg: String, errCode: Int) {
        }
    }
)</string,>
                    

The first two arguments are the Channel Type and the Channel ID (messaging and travel in this case). The Channel ID is optional; if you leave it out, the ID is determined based on the list of members. The channel type controls the settings we’re using for this channel.

There are 5 default types of channels:

  • livestream
  • messaging
  • team
  • gaming
  • commerce

These five options above provide you with the most sensible defaults for those use cases. You can also define custom channel types if Stream Chat defaults don’t work for your use-case.

The third argument is an object containing the channel data. You can add as many custom fields as you would like as long as the total size of the object is less than 5KB.

Messages

Now that we have the channel set up, let's send our first chat message:


const text = 'I’m mowing the air Rand, I’m mowing the air.';
const response = await channel.sendMessage({
    text,
    customfield: '123',
});
                    

// prepare the message
Message message = new Message();
message.setText("hello world");

// send the message to the channel
channel.sendMessage(message,
  new MessageCallback() {
      @Override
      public void onSuccess(MessageResponse response) {        
      }

      @Override
      public void onError(String errMsg, int errCode) {
      }
  });
                    

// The Stream Chat based on RxSwift framework.
import RxSwift

// Create an instance variable for the disposeBag in your view controller.
// It needs for the memory management of RxSwift subscriptions.
let disposeBag = DisposeBag()

// Create and send a message.
let message = Message(text: "Hello world!")

channel.send(message: message)
    .subscribe(onNext: { messageResponse in
        print(messageResponse)
    })
    .disposed(by: disposeBag)
                    

// prepare the message
val message = Message()
message.setText("hello world")

// send the message to the channel
channel.sendMessage(message, object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {
        println(response)
    }

    override fun onError(errMsg: String, errCode: Int) {
        println("$errMsg: $errCode")
    }
}
                    

Similar to users and channels, the sendMessage method allows you to add custom fields. When you send a message to a channel, Stream Chat automatically broadcasts to all the people that are watching this channel and updates in real-time.

Events

This is how you can listen to events on the clients-side:


channel.on('message.new', event =&gt; {
    console.log('received a new message', event.message.text);
    console.log(`Now have ${channel.state.messages.length} stored in local state`);
});
                    

Integer channelSubscriptionId = channel.addEventHandler(new ChatChannelEventHandler() {
  @Override
  public void onMessageNew(Event event) {
      event.getMessage();
  }
}
// channel.removeEventHandler(channelSubscriptionId) to remove it
                    

channel.onEvent(.messageNew)
    .subscribe(onNext: { event in
        print(event)
    })
    .disposed(by: disposeBag)
                    

val channelSubscriptionId = client.addEventHandler(object : ChatChannelEventHandler() {
    override fun onMessageNew(event: Event) {
        println(event)
    }
})
// channel.removeEventHandler(channelSubscriptionId) to remove
                    
You can receive the event and access the full channel state via channel.state.

Conclusion

Now that you understand the building blocks of a fully functional chat integration, let’s move on to the next sections of the documentation, where we dive deeper into details on each API endpoint.

API Tour

The interactive API tour is the fastest way to learn how Stream’s Chat API works. Be sure to check that out if you haven’t done so already.

CHAT API TOUR

Introduction

This guide quickly brings you up to speed on Stream’s Chat API. The API is flexible and allows you to build any type of chat or messaging.

You're currently not logged in. Create an account to automatically add your API key and secret to these code examples.

Setup

The Stream Chat API client is available as an npm package and also available via yarn.


# using npm
npm install stream-chat

# using yarn
yarn add stream-chat
                    

After installing the package, import the StreamChat module into your project, and you're ready to go:


import { StreamChat } from 'stream-chat';

// or

const StreamChat = require('stream-chat').StreamChat;
                    

The Stream Chat API client is available as a library using Jitpack package repository

Add repository into root build.gradle


allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}
                    

Add library dependency into app build.gradle


android {
    ...
    dataBinding {
        enabled = true
    }
  
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com.github.getstream:stream-chat-android:3.4.1'
}
                    

The Stream Chat API client is available as a library using Jitpack package repository

Add repository into root build.gradle


allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}
                    

Add library dependency into app build.gradle


android {
    ...
    dataBinding {
        enabled = true
    }
  
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {
    implementation 'com.github.getstream:stream-chat-android:3.4.1'
                    

You can add StreamChat to your Xcode project using CocoaPods or with Carthage.

CocoaPods

Add this entry in your Podfile and then run pod install


pod 'StreamChat'
                    

Carthage

To integrate Stream Chat into your Xcode project using Carthage, specify it in your Cartfile:


github "GetStream/stream-chat-swift"
                    

Then run carthage update --platform iOS --new-resolver and follow these steps:

  • Open your Xcode project

  • Select the project in the Navigator

  • Select your app target

  • Open General panel

  • Click the + button in the Linked Frameworks and Libraries section

  • Click the Add Other... and add StreamChatCore.framework in <Path to your Project>/Carthage/Build/iOS/

  • Add StreamChat.framework

  • Open Build Phases panel

  • Click the + button and select New Run Script Phase

  • Set the content to: /usr/local/bin/carthage copy-frameworks

  • Add to Input Files

    • $(SRCROOT)/Carthage/Build/iOS/StreamChatCore.framework

    • $(SRCROOT)/Carthage/Build/iOS/StreamChat.framework

  • Add to Output Files

    • $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/StreamChatCore.framework

    • $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/StreamChat.framework

Initialization & Users

Initialization for Browser & Mobile


const chatClient = new StreamChat('YOUR_API_KEY', {
    timeout: 3000,
    httpAgent: new http.Agent({ keepAlive: 3000 }),
    httpsAgent: new http.Agent({ keepAlive: 3000 }),
});
                    

// typically done in your BaseApplication class
StreamChat.init("qk4nn7rpcn75", new ApiClientOptions.Builder().Timeout(6666).build(), getApplicationContext());

// set the user to establish the websocket connection
// usually done when you open the chat interface
Client client = StreamChat.getInstance(getApplication());
                    

import StreamChatCore

// In your AppDelegate:

// Setup the Stream Chat Client.
Client.config = .init(apiKey: "&lt;#API_KEY#&gt;")

// Note: If you want to enable logs for requests and events you can enable them with extra parameter:  `logOptions`.
                    

// typically done in your BaseApplication class
StreamChat.init(
    "qk4nn7rpcn75", ApiClientOptions.Builder().Timeout(6666).build(),
    applicationContext
)

// set the user to establish the websocket connection
// usually done when you open the chat interface
val client = StreamChat.getInstance(application)
                    

The code above creates a chat client instance for browser/mobile usage. Additional options, such as API base URL and request timeouts, can be provided to the client.Setting.

Setting the user

Once initialized, you must specify the current user with setUser:


await chatClient.setUser(
    {
        id: 'john',
        name: 'John Doe',
        image: 'https://getstream.io/random_svg/?name=John',
    },
    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiamxhaGV5In0.OkDbpbujWJ-XIVHaf00Dnqt3v8Yp_nQ6CGzm-Z4QUVc',
);
                    

// this hashmap allows you to add any custom fields you want to store about your user
// the UI components will pick up name and image by default

HashMap<string, object=""> extraData = new HashMap&lt;&gt;();
extraData.put("name", "Bender");
extraData.put("image", "https://bit.ly/321RmWb");

User user = new User(USER_ID, extraData);
client.setUser(user, "FEED_USER_TOKEN", new ClientConnectionCallback() {
    @Override
    public void onSuccess(User user) {
        Log.i(TAG, String.format("Connection established for user %s", user.getName()));
    }

    @Override
    public void onError(String errMsg, int errCode) {
        Log.e(TAG, String.format("Failed to establish websocket connection: %s", errMsg));
    }
});</string,>
                    

// Create a user, when he/she was logged in.
let user = User(id: "bender", name: "Bender", avatarURL: URL(string: "https://bit.ly/321RmWb")!)

// You can setup a user token in 2 ways.

// 1. Setup the current user with a token.
Client.shared.set(user: user, token: token)

// 2. Setup the current user with a token provider.
Client.shared.set(user: user) { tokenProvider in
    // Make a request here to your backend to get a token for the current user.
    tokenProvider(token)
}
                    

// this hashmap allows you to add any custom fields you want to store about your user
// the UI components will pick up name and image by default
val extraData = HashMap<string, any="">()
extraData.put("name", "Bender")
extraData.put("image", "https://bit.ly/321RmWb")
val user = User("USER_ID", extraData)
client.setUser(user, "FEED_USER_TOKEN", object : ClientConnectionCallback {
    override fun onSuccess(user: User) {
        Log.i(TAG,String.format("Connection established for user %s", user.name) )
    }

    override fun onError(errMsg: String, errCode: Int) {
        Log.e(TAG,
            String.format("Failed to establish websocket connection: message %s",
                errMsg
            )
        )
    }
})</string,>
                    

Note how we are waiting for the setUser API call to be completed before moving forward. You should always make sure to have the user set before making any more calls. All SDKs make this very easy and wait or queue requests until then.

Set User Parameters

Name Type Description Default Optional
user object The user object. Must have id field. It can have as many custom fields as you want, as long as the total size of the object is less than 5KB
userToken string The user authentication token. See below for details default

User Fields

The id field is the only required field for the user. There are a few other fields you should know about though:

Name Type Description Default Optional
id string The id field is required
name string The name of the user used by our component libraries. This is just a convention and not a required field.
image string The image for this user. This is used by our component libraries but other than that is not required.
invisible boolean Determines if the user should show as offline even when they are online (only visible on the event.me user info).
banned boolean True when the user was banned from all channels
last_active string Reserved field indicating when the user was last active.
online boolean Reserved field indicating if the user is currently online.
role string Reserved field indicating the user's role. Can be either admin, user or guest.
mutes array Reserved field containing a list of mutes by this user (only visible on the event.me user info).
created_at date Reserved field indicating when the user was created.
updated_at date Reserved field indicating when the user was last updated.

Tokens

Tokens are used to authenticate the user. Typically, you send this token from your backend to the client-side when a user registers or logs in.

You can generate tokens server side with the following syntax:


const client = new StreamChat('', '{{ chat_api_secret }}');
const token = client.createToken('john');
                    

# pip install stream-chat

import stream_chat

chat_client = stream_chat.StreamChat(api_key="STREAM_KEY", api_secret="STREAM_SECRET")
token = chat_client.create_token('john')
                    

# gem install stream-chat-ruby

require 'stream-chat'

client = StreamChat::Client.new(api_key='STREAM_KEY', api_secret='STREAM_SECRET')
client.create_token('john')
                    

// composer require get-stream/stream-chat

$client = new GetStream\StreamChat\Client("STREAM_API_KEY", "STREAM_API_SECRET");
$token = $client->createToken("john");
                    

// at the moment we don't have a Java client for server side usage
                    

// go get github.com/GetStream/stream-chat-go

client, _ := stream.NewClient(APIKey, []byte(APISecret))
token := client.CreateToken("john", null)
                    

// at the moment we don't have a Swift client for server side usage
                    

// nuget install stream-chat-net

using StreamChat;

var client = new Client("API KEY", "API SECRET");
var token = client.CreateUserToken("john");
                    

By default, user tokens are valid indefinitely. You can set an expiration to tokens by passing it as the second parameter. The expiration should contain the number of seconds since the epoch.


// creates a token that expires in 1 hour using moment.js
const timestamp = moment().add('1h').format('X');
const token1 = client.createToken('john', timestamp);

// the same can be done with plain javascript
const token2 = client.createToken('john', Math.floor(Date.now() / 1000) + (60 * 60));
                    

# creates a token valid for 1 hour
token = chat_client.create_token(
    'john',
    exp=datetime.datetime.utcnow() + datetime.timedelta(hours=1)
)
                    

# creates a token valid for 1 hour
client.create_token('john', exp=Time.now.to_i + 3600)
                    

// creates a token valid for 1 hour
$expiration = (new DateTime())->getTimestamp() + 3600;
$token = $client->createToken("john", $expiration);
                    

// at the moment we don't have a Java SDK for server side integrations
                    

// creates a token valid for 1 hour
token := client.CreateToken("john", time.Now().UTC().Add(time.Hour))
                    

// at the moment we don't have a Swift client for server side usage
                    

// creates a token valid for 1 hour
var token = client.CreateUserToken("john", DateTime.Now.AddHours(1));
                    

Development Tokens

For development applications, it is possible to disable token authentication and use client-side generated tokens. Disabling auth checks is not suitable for a production application and should only be done for proofs-of-concept and applications in the early development stage. To enable development tokens, you need to change your application configuration.


await chatClient.setUser(
    {
        id: 'john',
        name: 'John Doe',
        image: 'https://getstream.io/random_svg/?name=John',
    },
   chatClient.devToken('john'),
);
                    

User user = new User(USER_ID, extraData);
client.setUser(user, client.devToken(USER_ID), new ClientConnectionCallback() {
    @Override
    public void onSuccess(User user) {
        Log.i(TAG, String.format("Connection established for user %s", user.getName()));
    }

    @Override
    public void onError(String errMsg, int errCode) {
        Log.e(TAG, String.format("Failed to establish websocket connection: %s", errMsg));
    }
});
                    

// Create a user, when he/she was logged in.
let user = User(id: "bender", name: "Bender", avatarURL: URL(string: "https://bit.ly/321RmWb")!)

// Setup the current user with a development token.
Client.shared.set(user: user, token: .development)
                    

val user = User("USER_ID", extraData)
client.setUser(user, client.devToken(USER_ID), object : ClientConnectionCallback {
    override fun onSuccess(user: User) {
        Log.i(TAG,String.format("Connection established for user %s", user.name) )
    }

    override fun onError(errMsg: String, errCode: Int) {
        Log.e(TAG,
            String.format("Failed to establish websocket connection: %s", errMsg
            )
        )
    }
})
                    
The above code used the setUser call. The setUser call is the most convenient option when your app has authenticated users. Alternatively, you can use setGuestUser if you want to allow users to chat with a guest account or the setAnonymousUser if you want to allow anonymous users to watch the chat.

Guest Users

Guest sessions can be created client-side and do not require any server-side authentication.

Guest users have a limited set of permissions. You can read more about how to configure permissions here. You can create a guest user session by using setGuestUser instead of setUser.


await client.setGuestUser({ id: 'tommaso' });
                    

// Not supported yet
                    

// Create a user, when he/she was logged in.
let user = User(id: "bender", name: "Bender", avatarURL: URL(string: "https://bit.ly/321RmWb")!)

// Setup the current user and guest token.
Client.shared.set(user: user, token: .guest)
                    

// Not supported yet
                    
The user object schema is the same as the one described in the Setting the user portion of the docs.


# the guest endpoint returns a user object and a token:
curl -i -X POST -d "{\"user\":{\"id\":\"tommaso\"}}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: anonymous" \
    "https://chat-us-east-c1.stream-io-api.com/guest?api_key=c38bmrz86x8y"
                    

Anonymous Users

If a user is not logged in, you can call the setAnonymousUser method. While you’re anonymous, you can’t do much, but for the livestream channel type, you’re still allowed to read the chat conversation.


await client.setAnonymousUser();
                    

// not supported yet
                    

// not supported yet
                    

// not supported yet
                    

Logging Out

To disconnect a user (say that you’re for instance logging out and logging in as someone new) you can call the disconnect method and repeat the setUser call as someone else:


client.disconnect();
client.setUser(
    {
        id: 'jack',
        name: 'Jack Doe',
    },
    'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiamxhaGV5In0.OkDbpbujWJ-XIVHaf00Dnqt3v8Yp_nQ6CGzm-Z4QUVc',
);
                    

client.disconnect();
                    

Client.shared.disconnect()
                    

client.disconnect(
                    

Querying Users

The Query Users method allows you to search for users and see if they are online/offline. The example below shows how you can retrieve the details for 3 users in one API call:


const response = await client.queryUsers({ id: { $in: ['john', 'jack', 'jessie'] } });
                    

ArrayList<string> searchUsersList = new ArrayList&lt;&gt;();
searchUsersList.add("john");
searchUsersList.add("jack");
searchUsersList.add("jessie");

FilterObject filter = Filters.in("id", searchUsersList);
client.queryUsers(new QueryUserRequest(filter, new QuerySort()), new QueryUserListCallback() {
    @Override
    public void onSuccess(QueryUserListResponse response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});</string>
                    

Client.shared.users(query: UsersQuery(filter: .key("id", .in(["john", "jack", "jessie"]))))
    .subscribe()
    .disposed(by: disposeBag)
                    

client.queryUsers(QueryUserRequest(
    Filters.`in`("id", listOf("john", "jack", "jessie")),
    QuerySort()
), object : QueryUserListCallback {
    override fun onSuccess(response: QueryUserListResponse?) {
        println(response)
    }

    override fun onError(errMsg: String?, errCode: Int) {
        println("$errMsg: $errCode")
    }
                    

const response = await client.queryUsers(
    { id: { $in: ['jessica'] } },
    { last_active: -1},
    { presence: true },
);
                    

ArrayList<string> searchUsersList = new ArrayList&lt;&gt;();
searchUsersList.add("jessica");

QueryUserRequest request = new QueryUserRequest(filter, sort).withPresence();
FilterObject filter = Filters.in("id", searchUsersList);
QuerySort sort = new QuerySort().desc("last_active");

client.queryUsers(request, new QueryUserListCallback() {
    @Override
    public void onSuccess(QueryUserListResponse response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});</string>
                    

// Create a user query.
let query = UsersQuery(filter: .key("id", .in(["john", "jack", "jessie"])),
                       sort: [Sorting("last_active")],
                       options: .presence)

Client.shared.users(query: query)
    .subscribe()
    .disposed(by: disposeBag)
                    

client.queryUsers(QueryUserRequest(
        Filters.`in`("id", listOf("jessica")),
        QuerySort().desc("last_active")
    ).withPresence(), 
    object : QueryUserListCallback {
        override fun onSuccess(response: QueryUserListResponse?) {
            println(response)
        }

        override fun onError(errMsg: String?, errCode: Int) {
            println("$errMsg: $errCode")
        }
}
                    

Another option is to query for banned users. This can be done with the following code snippet:


const banned = await client.queryUsers({ id: 'jessica', banned: true });
                    

An object with an array of users will be returned.

All filters use a Mongoose style syntax; however, we do not run MongoDB on the backend, so you can only use a subset of queries that are normally available within Mongoose.

You can filter and sort on the custom fields you've set for your user, the user id, and when the user was last active.

The options for the queryUser method are presence, limit, and offset. If presence is true this makes sure you receive the user.presence.changed event when a user goes online or offline.

Name Type Description Default Optional
presence boolean Get updates when the user goes offline/online false
limit integer Number of users to return 30
offset integer Offset for pagination 0
You can subscribe to presence status of at most 30 users using this method.

Querying Using the $autocomplete Operator

You can autocomplete the results of your user query by name, username, and/or ID.

1. By Name

If you want to return all users whose name includes 'ro', you could do so with the following:


const response = await serverClient.queryUsers({ name: { $autocomplete: 'ro' } });
                    

This would return an array of any matching users, such as:


[
    {
        id: userID,
        unique,
        name: 'Curiosity Rover',
    },
    {
        id: userID2,
        unique,
        name: 'Roxy',
    },
    {
        id: userID3,
        unique,
        name: 'Roxanne',
    },
]
                    

Here are a couple of examples of querying by either username or ID. They will also return an array of any matching users.

2. By Username


const response = await client.queryUsers({ username: { $autocomplete: 'rove' } });
                    

3. By ID


const response = await client.queryUsers({ id: { $autocomplete: 'USER_ID' } });
                    

Using the search() Method

An alternative is to use the search() method on the authClient. This method takes 3 arguments in this order:

  1. Filters

  2. The search itself

  3. An options object which can include the limit and offset.


const filters = { type: 'messaging' };

return await authClient.search(
  filters, 
  'supercalifragilisticexpialidocious', 
  {
    limit: 2,
    offset: 0,
  }
);
                    

This search will query all messaging channels for the string 'supercalifragilisticexpialidocious', while limiting the result to a length of 2, and no offset.

Updating Users

Stream Chat exposes a setUser method that automatically creates and updates the user. Please note that the setUser call has some limitations:

For example, if you're looking to sync your user base, you'll want to use the updateUser(s) method instead. It will also remove fields from the user if you don't send them in this update call. The updateUser endpoint is allowed to change a user's role and make them an admin.

For example:


const response = await client.setUser(
    { 
        id: userID, 
        role: 'admin', 
        favorite_color: 'green'
    },
    token
);
// user object is now {id: userID, role: 'user', favorite_color: 'green'}
// note how you are not allowed to make the user admin via this endpoint
const updateResponse = await serverClient.updateUsers([{ 
    id: userID, 
    role: 'admin', 
    book: 'dune'
 }]);
// user object is now {id: userID, role: 'admin', book: 'dune'}
// note how the user became admin and how the favorite_color field was removed
                    

# user object is now {id: userID, role: 'admin', book: 'dune'}
client.update_users([{"id": user_id, "role": "admin", "book": "dune"}])
                    

# user object is now {id: userID, role: 'admin', book: 'dune'}
client.update_users([{ id => userID, role => 'admin', book => 'dune'}])
                    

// user object is now {id: userID, role: 'admin', book: 'dune'}
$client->updateUsers(['id' => userID, 'role' => 'admin', 'book' => 'dune']);
                    

// at the moment we don't have a Java client for server side usage
                    

// user object is now {id: userID, role: 'admin', book: 'dune'}
client.UpdateUsers([]User{
    {ID: userID, Role: "admin", ExtraData: map[string]interface{}{"book": "dune"}},
})
                    

let user = User(id: "bob", name: "Bob")

Client.shared.update(user: user)
    .subscribe()
    .disposed(by: disposeBag)
                    

var user = new User()
{
    ID = "bob-1",
    Role = Role.Admin,
};
user.SetData("book", "dune");

// user object is now {id: userID, role: 'admin', book: 'dune'}
await client.Users.UpdateMany(new User[] { user });
                    
The updateUser (server-side) method has permission to make a user an admin; however, the setUser (client-side) method does not have permission. This is because the setUser method is called client-side, and for security reasons, you cannot edit a user's role from the client. The second difference is that the updateUser call can remove fields. This is why the favorite_color property in the above example is removed after the update is called.

Partial Update

If you need to update a subset of properties for a user(s), you can use a partial update method. Both set and unset parameters can be provided to add, modify, or remove attributes to or from the target user(s). The set and unset parameters can be used separately or combined.

Please see below for an example:


// make partial update call for userID
// it set's user.role to "admin", sets  user.field = {'text': 'value'}
// and user.field2.subfield = 'test'.
//
// NOTE: 
// changing role is available only for server-side auth.
// field name should not contain dots or spaces, as dot is used as path separator.
const update = {
    id: "userID",
    set: {
        role: "admin",
        field: {
            text: 'value'
        },
        'field2.subfield': 'test',
    },
    unset: ['field.unset'],
};
// response will contain user object with updated users info
const response = await client.partialUpdateUser(update);

// partial update for multiple users
const updates = [{
    id: "userID",
    set: {
        field: "value"
    }
}, {
    id: "userID2",
    unset: ["field.value"],
}];

// response will contain object {userID => user} with updated users info
const response = await client.partialUpdateUsers(updates);
                    

# make partial update call for userID
# it set's user.role to "admin", sets  user.field = {'text': 'value'}
# and user.field2.subfield = 'test'.

# NOTE: 
# changing role is available only for server-side auth.
# field name should not contain dots or spaces, as dot is used as path separator.

update = {
    "id": "userID",
    "set": {
        "role": "admin",
        "field": {
            "text": 'value'
        },
        'field2.subfield': 'test',
    },
    "unset": ['field.unset'],
};

# response will contain user object with updated users info
client.update_user_partial(update);

# partial update for multiple users
updates = [
    {
        "id": "userID",
        "set": {"field": "value"}
    },
    {
        "id": "userID2",
        "unset": ["field.value"]
    }
]

# response will contain object {userID => user} with updated users info
client.update_users_partial(updates)
                    

# at the moment this is not supported on Ruby
                    

// at the moment this is not supported on PHP
                    

// at the moment we don't have a Java client for server side usage
                    

// at the moment this is not supported on Go
                    

// at the moment we don't have a Swift client for server side usage
                    

// at the moment this is not supported on C#
                    

Send Message

Below is a detailed example of how to send a message using Stream Chat:


const response = await channel.sendMessage({
    text: '@Josh I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish.',
    attachments: [
        {
            type: 'image',
            asset_url: 'https://bit.ly/2K74TaG',
            thumb_url: 'https://bit.ly/2Uumxti',
            myCustomField: 123
        }
    ],
    mentioned_users: [josh.id],
    anotherCustomField: 234
});
                    

Message message = new Message();
message.setText("Josh I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish.");
HashMapextraDataMessage = new HashMap<>();
extraDataMessage.put("anotherCustomField",234);
message.setExtraData(extraDataMessage);

// add an image attachment to the message
Attachment attachment = new Attachment();
attachment.setType("image");
attachment.setImageURL("https://bit.ly/2K74TaG");
attachment.setFallback("test image");
// add some custom data to the attachment
HashMap extraDataAttachment = new HashMap<>();
extraDataAttachment.put("myCustomField", 123);
attachment.setExtraData(extraDataAttachment);

message.setAttachments(Arrays.asList(attachment));

// include the user ID of the mentioned user
message.setMentionedUsersId(Arrays.asList("josh-id"));

channel.sendMessage(message,
    new MessageCallback() {
        @Override
        public void onSuccess(MessageResponse response) {

        }

        @Override
        public void onError(String errMsg, int errCode) {

        }
    }
);
                    

// First of let's create an extra data for a channel.
struct Product: Codable {
    let id: String
    let name: String
    let price: Int
}

struct AttachmentData: Codable {
    let info: String
}

// Update your extra data types for the decoding.
ExtraData.decodableTypes = [.channel(ChannelInfo.self),
                            .message(Product.self),
                            .attachment(AttachmentData.self)]

// Create an extra data for message.
let iPhone = Product(id: "iPhone12,3", name: "iPhone 11 Pro", price: 999)

// Create an extra data for the attachment.
let fish = AttachmentData(info: "Just a fish to increase sales.")

// Create attachment.
let attachment = Attachment(type: .image,
                            title: "A fish",
                            imageURL: URL(string: "https://bit.ly/2K74TaG")!,
                            extraData: fish)

// Create a message with the extra data and attachments.
let message = Message(text: "We have a new iPhone 11 Pro. Do you want to buy it?",
                      attachments: [attachment],
                      extraData: iPhone,
                      mentionedUsers: [User(id: "josh-id", name: "Josh")])

channel.send(message: message).subscribe().disposed(by: disposeBag)
                    

val message = Message()
message.text =
    "Josh I told them I was pesca-pescatarian. Which is one who eats solely fish who eat other fish."
val extraDataMessage = HashMap()
extraDataMessage.put("anotherCustomField", 234)
message.extraData = extraDataMessage

// add an image attachment to the message
val attachment = Attachment()
attachment.setType("image")
attachment.setImageURL("https://bit.ly/2K74TaG")
attachment.setFallback("test image")
// add some custom data to the attachment
val extraDataAttachment = HashMap()
extraDataAttachment.put("myCustomField", 123)
attachment.setExtraData(extraDataAttachment)

message.attachments = Arrays.asList(attachment)

// include the user ID of the mentioned user
message.setMentionedUsersId(Arrays.asList("josh-id"))

channel.sendMessage(message,
    object : MessageCallback {
        override fun onSuccess(response: MessageResponse) {
            println(response)
        }

        override fun onError(errMsg: String, errCode: Int) {
            println("$errMsg: $errCode")
        }
    }
)
                    

There are five built-in fields for the message:

Name Type Description Default Optional
text string The text of the chat message (Stream chat supports markdown and automatically enriches URLs).
attachments array A list of attachments (audio, videos, images, and text). Max is 10 attachments per message. Each attachment can have up to 5KB.
user object This value is automatically set in client-side mode. You only need to send this value when using the server-side APIs.
mentioned_users array A list of users mentioned in the message. You send this as a list of user IDs and receive back the full user data.
message custom data object Extra data for the message. Must not exceed 5KB in size.

Note that both the message and the attachments can contain custom fields. By default Stream’s frontend components support the following attachment types:

  • Audio
  • Video
  • Image
  • Text

You can specify different types as long as you implement the frontend rendering logic to handle them. Common use cases include:

  • Embedding products (photos, descriptions, outbound links, etc.)
  • Sharing of a users location

The React tutorial for Stream Chat explains how to customize the Attachment component.

Get a Message

You can get a single message by its ID using the getMessage call:


await client.getMessage(messageID);
                    

client.getMessage(messageId, new MessageCallback(){
    @Override
    public void onSuccess(MessageResponse response) {
        
    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

Client.shared.message(with: messageId)
    .subscribe(onNext: { response in
        print(response)
    })
    .disposed(by: disposeBag)
                    

channel.getMessage(messageID, object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {}

    override fun onError(errMsg: String, errCode: Int) {}
}
                    

Message Format

When you post a message on a Channel, there are a few things that happen on the server:

  1. The text markdown format is parsed.
  2. The first URL found in message.text is enriched, and additional information is added automatically. This gives you a preview of the images, videos, etc. from the open-graph data on the associated page.
  3. Any slash commands such as /giphy, /imgur, /ban, /flag etc. are handled.

Messages containing URLs will have a generated attachment with the following structure:

Name Type Description Default Optional
type string The attachment type based on the URL resource. This can be: audio, image or video
author_name string The name of the author.
title string The attachment title.
title_link string The link to which the attachment message points to.
text string The attachment text. It will be displayed in the channel next to the original message.
image_url string The URL to the attached image. This is present for URL pointing to an image article (eg. Unsplash)
thumb_url string The URL to the attached file thumbnail. You can use this to represent the attached link.
asset_url string The URL to the audio, video or image related to the URL.
og_scrape_url string The original URL that was used to scrape this attachment.

Below is an example of URL enrichment as well as the resulting message structure:


const response = await channel.sendMessage({
    text: 'Check this bear out https://imgur.com/r/bears/4zmGbMN'
})

// response message object
{
    "id": "thierry-5e9619ec-1a0d-443b-ab26-c597ed7af3d0",
    "text": "Check this bear out https://imgur.com/r/bears/4zmGbMN",
    "html": "

Check this bear out https://imgur.com/r/bears/4zmGbMN

\n", "type": "regular", "user": { "id": "thierry", "role": "user", "created_at": "2019-04-03T14:42:47.087869Z", "updated_at": "2019-04-16T09:20:03.982283Z", "last_active": "2019-04-16T11:23:51.168113408+02:00", "online": true }, "attachments": [ { "type": "image", "author_name": "Imgur", "title": "An update: Dushi made it safe to Bear Sanctuary Müritz", "title_link": "https://imgur.com/4zmGbMN", "text": "1678 views on Imgur", "image_url": "https://i.imgur.com/4zmGbMN.jpg?fb", "thumb_url": "https://i.imgur.com/4zmGbMN.jpg?fb", "og_scrape_url": "https://imgur.com/r/bears/4zmGbMN" } ], "latest_reactions": [], "own_reactions": [], "reaction_counts": null, "reply_count": 0, "created_at": "2019-04-16T09:40:04.665274Z", "updated_at": "2019-04-16T09:40:04.665274Z" }

Message message = new Message();
message.setText("Check this bear out https://imgur.com/r/bears/4zmGbMN");

// send the message to the channel
channel.sendMessage(message,
  new MessageCallback() {
      @Override
      public void onSuccess(MessageResponse response) {        
      }

      @Override
      public void onError(String errMsg, int errCode) {
      }
});

// response message object
//    {
//        "id": "thierry-5e9619ec-1a0d-443b-ab26-c597ed7af3d0",
//            "text": "Check this bear out https://imgur.com/r/bears/4zmGbMN",
//            "html": "

Check this bear out https://imgur.com/r/bears/4zmGbMN

\n", // "type": "regular", // "user": { // "id": "thierry", // "role": "user", // "created_at": "2019-04-03T14:42:47.087869Z", // "updated_at": "2019-04-16T09:20:03.982283Z", // "last_active": "2019-04-16T11:23:51.168113408+02:00", // "online": true // }, // "attachments": [ // { // "type": "image", // "author_name": "Imgur", // "title": "An update: Dushi made it safe to Bear Sanctuary Müritz", // "title_link": "https://imgur.com/4zmGbMN", // "text": "1678 views on Imgur", // "image_url": "https://i.imgur.com/4zmGbMN.jpg?fb", // "thumb_url": "https://i.imgur.com/4zmGbMN.jpg?fb", // "og_scrape_url": "https://imgur.com/r/bears/4zmGbMN" // } // ], // "latest_reactions": [], // "own_reactions": [], // "reaction_counts": null, // "reply_count": 0, // "created_at": "2019-04-16T09:40:04.665274Z", // "updated_at": "2019-04-16T09:40:04.665274Z" // }

// TODO
                    

Message message = new Message();
message.setText("Check this bear out https://imgur.com/r/bears/4zmGbMN");

// send the message to the channel
channel.sendMessage(message,
  new MessageCallback() {
      @Override
      public void onSuccess(MessageResponse response) {        
      }

      @Override
      public void onError(String errMsg, int errCode) {
      }
  });


// response message object
//    {
//        "id": "thierry-5e9619ec-1a0d-443b-ab26-c597ed7af3d0",
//            "text": "Check this bear out https://imgur.com/r/bears/4zmGbMN",
//            "html": "

Check this bear out https://imgur.com/r/bears/4zmGbMN

\n", // "type": "regular", // "user": { // "id": "thierry", // "role": "user", // "created_at": "2019-04-03T14:42:47.087869Z", // "updated_at": "2019-04-16T09:20:03.982283Z", // "last_active": "2019-04-16T11:23:51.168113408+02:00", // "online": true // }, // "attachments": [ // { // "type": "image", // "author_name": "Imgur", // "title": "An update: Dushi made it safe to Bear Sanctuary Müritz", // "title_link": "https://imgur.com/4zmGbMN", // "text": "1678 views on Imgur", // "image_url": "https://i.imgur.com/4zmGbMN.jpg?fb", // "thumb_url": "https://i.imgur.com/4zmGbMN.jpg?fb", // "og_scrape_url": "https://imgur.com/r/bears/4zmGbMN" // } // ], // "latest_reactions": [], // "own_reactions": [], // "reaction_counts": null, // "reply_count": 0, // "created_at": "2019-04-16T09:40:04.665274Z", // "updated_at": "2019-04-16T09:40:04.665274Z" //

Messages returned by the API follow this structure:

Name Type Description Default Optional
id string The message ID. This is either created by Stream or set client side when the message is added.
html string The safe HTML generated from the raw text message. This field can only be set using server-side APIs or via the import
type string The message type. See below for more information.
user object The author user object. Schema is as described in the Setting the user portion of the docs.
attachments array The list of attachments, either provided by the user or generated from a command or as a result of URL scraping.
latest_reactions array The latest reactions to the message created by any user.
own_reactions array The reactions added to the message by the current user. e.g. ["haha", "angry"].
reaction_counts object The reaction count by type for this message e.g. {"haha": 3, "angry": 2}.
reply_count integer Reserved field indicating the number of replies for this message.
parent_id string The ID of the parent message, if the message is a reply.
created_at date Reserved field indicating when the message was created.
updated_at date Reserved field indicating when the message was updated last time.
deleted_at date Reserved field indicating when the message was deleted.
mentioned_users array of users The list of users that are mentioned in this message.

Message Types

Chat supports different types of messages. The type of the message is set by the APIs or by chat bots and custom commands.

Name

Description

regular

A regular message created in the channel.

ephemeral

A temporary message which is only delivered to one user. It is not stored in the channel history. Ephemeral messages are normally used by commands (e.g. /giphy) to prompt messages or request for actions.

error

An error message generated as a result of a failed command. It is also ephemeral, as it is not stored in the channel history and is only delivered to one user.

reply

A message in a reply thread. Messages created with parent_id are automatically of this type.

system

A message generated by a system event, like updating the channel or muting a user.

Update a Message

You can edit a message by calling updateMessage and including a message with an ID – the ID field is required when editing a message:


const message = { id: 123, text: 'the edited version of my text' };
const updateResponse = await client.updateMessage(message);
                    

// update some field of the message
message.setText("my updated text");

// send the message to the channel
client.updateMessage(message, new MessageCallback(){
    @Override
    public void onSuccess(MessageResponse response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

let editedMessage = Message(id: message.id, text: newText)
channel.send(message: editedMessage).subscribe().disposed(by: disposeBag)
                    

// update some field of the message
message.setText("my updated text");

// send the message to the channel
channel.updateMessage(message, object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {}

    override fun onError(errMsg: String, errCode: Int) {}
}
                    

Delete a Message

You can delete a message by calling removeMessage  and including a message with an ID:


await client.deleteMessage(messageID);
                    

client.deleteMessage("messageID", new MessageCallback(){
    @Override
    public void onSuccess(MessageResponse response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

// Delete a message.
channel.delete(message: message).subscribe().disposed(by: disposeBag)
// or
message.delete().subscribe().disposed(by: disposeBag)
                    

channel.deleteMessage(messageID, object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {}

    override fun onError(errMsg: String, errCode: Int) {}
})
                    

File Uploads

The channel.sendImage and channel.sendFile methods make it easy to upload files. Note that this functionality defaults to using the Stream CDN. If you would like, you can easily change the logic to upload to your own CDN of choice.


const promises = [
    channel.sendImage(
        fs.createReadStream('./helloworld.jpg'),
        'hello_world1.jpg',
    ),
    channel.sendImage(
        fs.createReadStream('./helloworld.jpg'),
        'hello_world2.jpg',
    ),
];
const results = await Promise.all(promises);
const attachments = results.map(response => {
    return {
        type: 'image',
        thumb_url: response.file,
        asset_url: response.file,
    };
});
const response = await channel.sendMessage({
    text: 'Check out what I have uploaded in parallel',
    attachments,
});
expect(response.message.attachments).to.equal(attachments);
                    

// upload an image
channel.sendImage(filePath, new UploadFileCallback() {
            @Override
            public void onSuccess(Object o) {
                
            }

            @Override
            public void onError(String errMsg, int errCode) {

            }

            @Override
            public void onProgress(Object o) {

            }
});

// upload a file
channel.sendFile(filePath new UploadFileCallback() {
            @Override
            public void onSuccess(Object o) {
                
            }

            @Override
            public void onError(String errMsg, int errCode) {

            }

            @Override
            public void onProgress(Object o) {

            }
});
                    

// Upload UIImage.
if let imageData = image.jpegData(compressionQuality: 0.9) {
    channel.sendImage(fileName: "my_photo", 
                      mimeType: AttachmentFileType.jpeg.mimeType,
                      imageData: imageData)
        .observeOn(MainScheduler.instance)
        .subscribe(
            onNext: { response in
                print(response.progress)
            },
            onCompleted: { 
                print("The image was uploaded")
            })
        .disposed(by: disposeBag)
}

// Upload a local file with URL.
if let fileData = try? Data(contentsOf: url) {
    channel.sendFile(fileName: "my_file", 
                      mimeType: AttachmentFileType.generic.mimeType // "application/octet-stream"
                      imageData: fileData)
        .observeOn(MainScheduler.instance)
        .subscribe(
            onNext: { response in
                print(response.progress)
            },
            onCompleted: { 
                print("The file was uploaded")
            })
        .disposed(by: disposeBag)
}
                    

// upload an image
channel.sendImage(filePath, object : UploadFileCallback {
    override fun onSuccess(o: Object) {
       
    }

    override fun onError(errMsg: String, errCode: Int) {
        
    }

    override fun onProgress(o: Object) {
        
    }
})

// upload a file
channel.sendFile(filePath, object : UploadFileCallback {
    override fun onSuccess(o: Object) {
       
    }

    override fun onError(errMsg: String, errCode: Int) {
        
    }

    override fun onProgress(o: Object) {
        
    }
})
                    

In the example above, note how the message attachments are created after the files are uploaded. The React components support regular uploads, clipboard pasting, drag and drop, as well as URL enrichment via built-in open-graph scraping. As a bonus, the Stream CDN will automatically handle image resizing for you.

Send Reaction

Stream Chat has built-in support for user Reactions. Common examples are likes, comments, loves, etc. Reactions can be customized so that you are able to use any type of reaction your application requires.

Similar to other objects in Stream Chat, reactions allow you to add custom data to the reaction of your choice. This is helpful if you want to customize the reaction logic.

Custom data for reactions is limited to 1KB.


const reaction = await channel.sendReaction(messageID, {
    type: 'love',
    myCustomField: 123,
});
                    

Reaction reaction = new Reaction();
reaction.setMessageId("message-id-here");
reaction.setType("like");

channel.sendReaction(reaction, new MessageCallback() {
    @Override
    public void onSuccess(MessageResponse response) {
        
    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

message.addReaction(.like).subscribe().disposed(by: disposeBag)
                    

val reaction = new Reaction()
reaction.setMessageId("message-id-here");
reaction.setType("like");

channel.sendReaction(reaction, object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {}

    override fun onError(errMsg: String, errCode: Int) {}
})
                    

Removing a Reaction


await channel.deleteReaction(messageID, 'love');
                    

channel.deleteReaction(messageID, "like", new MessageCallback() {
    @Override
    public void onSuccess(MessageResponse response) {

    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

message.deleteReaction(.like).subscribe().disposed(by: disposeBag)
                    

channel.deleteReaction(messageID, "like", object : MessageCallback {
    override fun onSuccess(response: MessageResponse) {}

    override fun onError(errMsg: String, errCode: Int) {}
})
                    

Paginating Reactions

Messages returned by the APIs automatically include the 10 most recent reactions. You can also retrieve more reactions and paginate using the following logic:


// get the first 10 reactions
const response = await channel.getReactions(messageID, { limit: 5 });

// get 3 reactions past the first 10
const response = await channel.getReactions(messageID, { limit: 3, offset: 10 });
                    

// get the first 10 reactions
channel.getReactions(messageID, new GetReactionsCallback(){
        @Override
        public void onSuccess(GetReactionsResponse response) {
        }

        @Override
        public void onError(String errMsg, int errCode) {
        }
    }
);

// get 3 reactions past the first 10
channel.getReactions(messageID, new PaginationOptions.Builder().offset(10).limit(3).build(),
    new GetReactionsCallback(){
        @Override
        public void onSuccess(GetReactionsResponse response) {
        }

        @Override
        public void onError(String errMsg, int errCode) {
        }
    }
);
                    

// TODO
                    

// get the first 10 reactions
channel.getReactions(messageID, object : GetReactionsCallback {
    override fun onSuccess(response: GetReactionsResponse) {

    }

    override fun onError(errMsg: String, errCode: Int) {

    }
})

// get 3 reactions past the first 10
channel.getReactions(messageID, PaginationOptions.Builder().offset(10).limit(3).build(),
  object : GetReactionsCallback {
      fun onSuccess(response: GetReactionsResponse) {

      }

      fun onError(errMsg: String, errCode: Int) {

      }
})
                    

Threads & Replies

Threads and replies provide your users with a way to go into more detail about a specific topic.

This can be very helpful to keep the conversation organized and reduce noise. To create a thread you simply send a message with a parent_id. Have a look at the example below:


const replyResponse = await channel.sendMessage({
    text: 'replying to a message',
    parent_id: parentID,
    show_in_channel: false,
});
                    

// set the parent id to make sure a message shows up in a thread
Message message = new Message();
message.setText("hello world");
message.setParentId(parentMessage.getId());

// send the message to the channel
channel.sendMessage(message, new MessageCallback() {
    @Override
    public void onSuccess(MessageResponse response) {
        
    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

let message = Message(text: "Hello world!", parentId: parentMessage.id)
channel.send(message: message).subscribe().disposed(by: disposeBag)
                    

// set the parent id to make sure a message shows up in a thread
Message message = new Message();
message.setText("hello world");
message.setParentId(parentMessage.getId());

// send the message to the channel
channel.sendMessage(message, new MessageCallback() {
    override fun onSuccess(response: MessageResponse) {

    }

    override fun onError(errMsg: String, errCode: Int) {

    }
})
                    

If you specify show_in_channel, the message will be visible both in a thread of replies as well as the main channel.

Search

Message search is built-in to the chat API. You can enable and/or disable the search indexing on a per chat type basis. The command shown below selects the channels in which John is a member. Next, it searches the messages in those channels for the keyword “'supercalifragilisticexpialidocious'”:


const filters = { members: { $in: ['john'] } };
const response = await client.search(
   filters,
   'supercalifragilisticexpialidocious',
   { limit: 2, offset: 0 },
);
                    

final ArrayList searchUsersList = new ArrayList<>();
searchUsersList.add("john");

final FilterObject filter = Filters.in("members", searchUsersList);

SearchMessagesRequest searchMessagesRequest = new SearchMessagesRequest(filter,
        "supercalifragilisticexpialidocious")
        .withLimit(2)
        .withOffset(0);

client.searchMessages(searchMessagesRequest,
    new SearchMessagesCallback() {
        @Override
        public void onSuccess(SearchMessagesResponse response) {

        }

        @Override
        public void onError(String errMsg, int errCode) {

        }
});
                    

// Search with default filter where the current user is a member of channels.
// Default pagination is 20 messages.
Client.shared.search(query: "supercalifragilisticexpialidocious")
    .subscribe(onNext: { messages in
        print(messages)
    })
    .disposed(by: disposeBag)

// A custom filter and the next page for the results.
Client.shared.search(filter: filter, query: "supercalifragilisticexpialidocious", pagination: .limit(20) + .offset(20))
    .subscribe(onNext: { messages in
        print(messages)
    })
    .disposed(by: disposeBag)
                    

client.searchMessages(
  SearchMessagesRequest(
      Filters.`in`("members", listOf("john")),
      "supercalifragilisticexpialidocious")
      .withLimit(2)
      .withOffset(0),
  object : SearchMessagesCallback {
      override fun onSuccess(response: SearchMessagesResponse?) {
          
      }

      override fun onError(errMsg: String?, errCode: Int) {
     
      }
})
                    

Pagination works via the standard limit and offset parameters. The first argument, filter, uses a MongoDB style query expression.

We do not run MongoDB on the backend, so only a subset of the standard MongoDB filters are supported.

Event Object

Events

The following events are used by Stream's chat SDK. There are two type of events: client events and channel events.

Client events are events sent to notify users about other users or for notifications related to their own user. A user coming online or a notification that a user received an invite to a channel are client events.

Channel events are events sent to all users watching a channel where the event originated. These events always include the channel information.

Event

Trigger

Recipients

Type

user.presence.changed

when a user status changes (eg. online, offline, away, etc.)

clients subscribed to the user status

client event

user.watching.start

when a user starts watching a channel

clients watching the channel

channel event

user.watching.stop

when a user stops watching a channel

clients watching the channel

channel event

user.updated

when a user is updated

clients subscribed to the user status

client event

typing.start

sent when a user starts typing

clients watching the channel

channel event

typing.stop

sent when a user stops typing

clients watching the channel

channel event

message.new

when a new message is added on a channel

clients watching the channel

channel event

message.updated

when a message is updated

clients watching the channel

channel event

message.deleted

when a message is deleted

clients watching the channel

channel event

message.read

when a channel is marked as read

clients watching the channel

channel event

reaction.new

when a message reaction is added

clients watching the channel

channel event

reaction.updated

when a message reaction is updated

clients watching the channel

channel event

reaction.deleted

when a message reaction is deleted

clients watching the channel

channel event

member.added

when a member is added to a channel

clients watching the channel

channel event

member.removed

when a member is removed from a channel

clients watching the channel

channel event

channel.updated

when a channel is updated

clients watching the channel

channel event

health.check

every 30 second to confirm that the client connection is still alive

all clients

N/A

connection.changed

when the state of the connection changed

local event

N/A

connection.recovered

when the connection to chat servers is back online

local event

N/A

notification.message_new

when a message is added to a channel

clients that are not currently watching the channel

client event

notification.mark_read

when the total count of unread messages (across all channels the user is a member) changes

clients from the user with the new unread count

client event

notification.invited

when the user is invited to join a channel

clients from the user invited that are not watching the channel

client event

notification.invite_accepted

when the user accepts an invite

clients from the user invited that are not watching the channel

client event

notification.added_to_channel

when the user is added to the list of channel members

clients from the user added that are not watching the channel

client event

notification.removed_from_channel

when a user is removed from the list of channel members

clients from the user removed that are not watching the channel

client event

notification.channel_truncated

when a channels' history is truncated

clients from members that are not watching the channel

client event

notification.channel_deleted

when a channel is deleted

clients from members that are not watching the channel

client event

notification.mutes_updated

when the user mutes are updated

clients from the user that updated the list of mutes

client event

channel.visible

when a channel is made visible

clients from the user that marked the user as visible (see hiding channels)

channel event

channel.hidden

when a channel is mark as hidden

clients from the user that marked the user as hidden (see hiding channels)

channel event

channel.deleted

when a channel is deleted

clients watching the channel

channel event

channel.truncated

when a channels' history is truncated

clients watching the channel

channel event

Event Object

Name Type Description Default Optional
cid string Channel ID
type string Event type
message object Message Object
reaction object Reaction Object
channel object Channel Object
member object User object for the channel member that was added/removed
user object User object of the current user
me object User object of the health check user
total_unread_count int the number of unread messages for current user
watcher_count int Number of users watching this channel

Listening for Events

As soon as you call watch on a Channel or queryChannels you’ll start to listen to these events. You can hook into specific events:


channel.on('message.deleted', event => {
    console.log('event', event);
    console.log('channel.state', channel.state);
});
                    

Integer channelSubscriptionId = channel.addEventHandler(new ChatChannelEventHandler() {
  @Override
  public void onMessageNew(Event event) {
      event.getMessage();
  }
}
// channel.removeEventHandler(channelSubscriptionId) to remove it
                    

channel.onEvent(.messageNew)
    .subscribe(onNext: { event in
        print(event)
    })
    .disposed(by: disposeBag)
                    

val channelSubscriptionId = client.addEventHandler(object : ChatChannelEventHandler() {
    override fun onMessageNew(event: Event) {
        event.getMessage()
    }
})
// channel.removeEventHandler(channelSubscriptionId) to remove it
                    

You can also listen to all events at once:


channel.on(event => {
    console.log('event', event);
    console.log('channel.state', channel.state);
});
                    

Integer channelSubscriptionId = channel.addEventHandler(new ChatChannelEventHandler() {
  @Override
  public void onAnyEvent(Event event) {
      event.getType();
  }
}
                    

channel.onEvent()
    .subscribe(onNext: { event in
        print(event)
    })
    .disposed(by: disposeBag)
                    

channelSubscriptionId = channel.addEventHandler(object : ChatChannelEventHandler() {
  override fun onAnyEvent(event: Event) {
      event.getType();
  }
}
                    

Client Events

Not all events are specific to channels, events such as user status has changed, users' unread count has changed, and other notifications are sent as client events. These events should be registered on the client directly:


// subscribe to all client events and log the unread_count field
client.on(event => {
	if (event.total_unread_count != null) {
		console.log(`unread messages count is now: ${event.total_unread_count}`);
	}
	if (event.unread_channels != null) {
		console.log(`unread channels count is now: ${event.unread_channels}`);
	}
});

// the initial count of unread messages is returned by client.setUser
const r = await client.setUser(user, userToken);
console.log(`you have ${r.me.total_unread_count} unread messages on ${r.me.unread_channels} channels`);
                    

// receive an event whenever any of the channels that you are watching changes
Integer subscriptionId = client.addEventHandler(new ChatEventHandler() {
    @Override
    public void onMessageNew(Channel channel, Event event) {
        event.getMessage();
    }
});
                    

// Subscribe for user presence events.
Client.shared.onEvent(.userPresenceChanged)
    .subscribe(onNext: { event in
        print(event)
    })
    .disposed(by: disposeBag)

// Unread count globally.
Client.shared.unreadCount
    .subscribe(onNext: { unreadCount in
        print(unreadCount)
    })
    .disposed(by: disposeBag)

// Unread count for a channel.
channel.unreadCount
    .subscribe(onNext: { unreadCount in
        print(unreadCount)
    })
    .disposed(by: disposeBag)
                    

// receive an event whenever any of the channels that you are watching changes
val subscriptionId = client.addEventHandler(object : ChatEventHandler() {
    override fun onMessageNew(channel: Channel, event: Event) {
        event.getMessage()
    }
})
                    

Connection Events

The official SDKs make sure that a connection to Stream is kept alive at all times and that chat state is recovered when the user's internet connection comes back online. Your application can subscribe to changes to the connection using client events.


client.on('connection.changed', e => {
    if (e.online) {
        console.log('the connection is up!');
    } else {
        console.log('the connection is down!');
    };
});
                    

Integer subscriptionId = client.addEventHandler(new ChatEventHandler() {
    @Override
    public void onConnectionChanged(Event event) {
        event.getOnline();
    }
});
                    

// Get all connection states.
Client.shared.connection
    .subscribe(onNext: { connection in
        print(connection)
    })
    .disposed(by: disposeBag)

// Get an event when only the client is connected.
Client.shared.connection.connected()
    .subscribe(onNext: {
        print("You are online")
    })
    .disposed(by: disposeBag)
                    

val subscriptionId = client.addEventHandler(object : ChatEventHandler() {
    override fun onConnectionChanged(event: Event) {
        event.getOnline()
    }
})
                    

Stop Listening for Events

It is a good practice to de-register event handlers once they are not in use anymore. Doing so will save you from performance degradations coming from memory leaks or even from errors and exceptions (i.e. null pointer exceptions)


// remove the handler from all client events
client.off(myClientEventHandler);

// remove the handler from all events on a channel
channel.off(myChannelEventHandler);

// remove the handler for all "message.new" events on the channel
channel.off("message.new", myChannelEventHandler);
                    

// remove the event handler based on the subscription ID returned by `client.addEventHandler`
client.removeEventHandler(subscriptionId);

// remove the event handler based on the subscription ID returned by `channel.addEventHandler`
channel.removeEventHandler(subscriptionId);
                    

channel.stopWatching().subscribe().disposed(by: disposeBag)
                    

// remove the event handler based on the subscription ID returned by `client.addEventHandler`
client.removeEventHandler(subscriptionId)

// remove the event handler based on the subscription ID returned by `channel.addEventHandler`
channel.removeEventHandler(subscriptionId)
                    

Typing Events

All official SDKs support typing events out of the box and are handled out of the box on all channels with the typing_events featured enabled.

There are two types of events related to user typing: "typing.start" and "typing.stop", both are sent client-side in response to user input. If you are building your own chat integration on top of an API client instead of using an SDK, we recommend following a few basic rules:

  1. Send the "typing.start" only the first time a user starts typing
  2. Once the user stops typing for longer than two seconds, send the "typing.stop" event

// sends an event typing.start to all channel participants
await channel.keystroke();

// sends an event typing.stop to all channel participants
await channel.stopTyping();
                    

// sends a typing.start event if it's been more than 3000 ms since the last event
channel.keystroke(new EventCallback() {
    @Override
    public void onSuccess(EventResponse response) {
    }

    @Override
    public void onError(String errMsg, int errCode) {
    }

});

// sends an event typing.stop to all channel participants
channel.stopTyping(new EventCallback() {
    @Override
    public void onSuccess(EventResponse response) {
    }

    @Override
    public void onError(String errMsg, int errCode) {
    }

});
                    

// Send a typing start event.
channel.send(eventType: .typingStart).subscribe().disposed(by: disposeBag)

// Send a typing stop event.
channel.send(eventType: .typingStop).subscribe().disposed(by: disposeBag)
                    

// sends a typing.start event if it's been more than 3000 ms since the last event
channel.keystroke()

// sends an event typing.stop to all channel participants
channel.stopTyping()
                    

Notification Events

Notification events help you update your UI (web+mobile, multiple tabs, ...) even if you're not watching a particular channel. Notification events are sent to channel members that are not currently watching the channel. There are six notification events:

  1. notification.message_new
  2. notification.mark_read
  3. notification.invited
  4. notification.invite_accepted
  5. notification.added_to_channel
  6. notification.removed_from_channel
  7. notification.mutes_updated
  8. notification.channel_deleted
  9. notification.channel_truncated

Notification events are triggered on the client level. Here's an example of how you can render a new conversation when a user is added to a channel:


client.on('notification.added_to_channel', (event) => {
    console.log(`you were just added to channel ${event.channel}`)
});
                    

Integer subscriptionId = client.addEventHandler(new ChatEventHandler() {
  @Override
  public void onNotificationAddedToChannel(Channel channel, Event event) {

  }
});
                    

Client.shared.onEvent(.notificationAddedToChannel)
    .subscribe(onNext: { event in
        print(event)
    })
    .disposed(by: disposeBag)
                    

val subscriptionId = client.addEventHandler(object : ChatEventHandler() {
    override fun onNotificationAddedToChannel(channel: Channel, event: Event) {
       
    }
})
                    

Channel Initialization

Here’s how you can initialize a single channel:


const conversation = authClient.channel('messaging', 'thierry-tommaso-1', {
    name: 'Founder Chat',
    image: 'http://bit.ly/2O35mws',
    members: ['thierry', 'tommaso'],
});
                    

Channel Initialization Parameters

Name Type Description Default Optional
type string The channel type. Default types are livestream, messaging, team, gaming and commerce. You can also create your own types. -
id string The channel id (optional). If you don't specify an ID the ID will be generated based on the list of members. -
channel data string Extra data for the channel. Must not exceed 5KB in size. default
The channel data can contain any number of custom fields. However, there are a few reserved fields that Stream uses:

Reserved Fields for Custom Channel Data

<p>Name</p> <p>Type</p> <p>Description</p>
<p>name</p> <p>string</p> <p>The Channel name. No special meaning, but by default the UI component will try to render this if the property is present.</p>
<p>image</p> <p>string</p> <p>The Channel image. Again there is no special meaning but by default, the UI components will try to render this property if it exists</p>
<p>members</p> <p>array</p> <p>The members participating in this Channel. Note that you don’t need to specify members for a live stream or other public chat. You only need to specify the members if you want to limit access of this chat to these members and subscribe them to future updates</p>

Watching a Channel

Once you watch a channel, you will start receiving events for that channel. More info on that here.

To start watching a channel:


const conversationState = await conversation.watch();
                    

Response Schema

Name

Type

Description

config

object

The configuration for the channel type.

channel

object

The Channel object.

online

integer

Number of online members.

watchers

object

Users that are watching this channel. Represented as a mapping from the user id to the user object.

members

object

Channel members. Represented as a mapping from the user id to the user object.

read

object

Read messages grouped by user id. Represented as a mapping from the user id to the message object.


curl -i -X POST -d "{\"state\":true,\"subscribe\":true}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiamxhaGV5In0.P47UdkwvNDWPSXrUfg02pQjOzTGlhXVn0CQGAqaTxDE" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso/query?api_key=c38bmrz86x8y"
                    
Make sure that you call client.setUser() before channel.watch()

Unwatching

To stop watching a channel:


await conversation.stopWatching();
                    

curl -i -X POST -d "{\"user_id\":\"john\"}"\
      -H "Content-Type: application/json" \
      -H "Stream-Auth-Type: jwt" \
      -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiam9obiJ9._637-4WV1G8lbzfZmWkUZb7qB6_J7rGtzd8AIvbuOSY" \
      "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso/stop-watching?api_key=c38bmrz86x8y"
                    

Querying Channels

If you’re building a similar application to Facebook Messenger or Intercom, you’ll want to show a list of Channels. The Chat API supports MongoDB style queries to make this easy to implement.

You can query channels based on built-in fields as well as any custom field you add to channels. Multiple filters can be combined together using AND, OR logical operators, each filter can use its own comparison (equality, inequality, greater than, greater or equal, ...). You can find the complete list of supported operators in the query syntax section of the docs.

As an example, let's say that you want to query the last conversations I participated in sorted by last_message_at.

Stream Chat does not run MongoDB on the backend, only a subset of the query options are available.

Here’s an example of how you can query the list of channels:


const filter = { members: { $in: ['thierry'] } };
const sort = { last_message_at: -1 };

const channels = await authClient.queryChannels(filter, sort, {
    watch: true,
    state: true,
});

for (const c of channels) {
    console.log(c.custom.name, c.cid);
}
                    

Query Parameters

Name Type Description Default Optional
filters object The query filters to use. You can query on any of the custom fields you've defined on the Channel. You can also filter other built-in channel fields, see next section for reference. {}
sort objects The sorting uses for the channels matching the filters. Sorting is based on field and direction, multiple sorting options can be provided. You can sort based on last_updated, last_message_at, updated_at, created_at or member_count. Direction can be ascending (1) or descending (-1) {last_updated_at: -1}
options object Query options. See below. {}
By default when query channels does not have any filter and it will match all channels on your application. While this might be OK during development, you most likely want to have at least some basic filtering.

The query channels endpoint will only return channels that the user can read, you should make sure that the query uses a filter that includes such logic. For example: messaging channels are readable only to their members, such requirement can be included in the query filter (see below).

Common filters by use-case

Messaging and Team

On messaging and team applications you normally have users added to channels as a member. A good starting point is to use this filter to show the channels the user is participating.


filter = { members: { $in: ['thierry'] } };
                    

Support

On a support chat, you probably want to attach additional information to channels such as the support agent handling the case and other information regarding the status of the support case (ie. open, pending, solved).


filter = { agent_id: `${user.Id}`, status: {$in: ['pending', 'open', 'new']}};
                    

Channel Queryable Built-In Fields

The following channel fields can be used to filter your query results

Field Name

Type

Description

Example

frozen

boolean

channel frozen status

false

type

string

the type of channel

messaging

id

string

the ID of the channel

general

cid

string

the full channel ID

messaging:general

members

string or list of string

the list of user IDs of the channel members

marcelo or [thierry, marcelo]

invite

string, must be one of these values: (pending, accepted, rejected)

the status of the invite

pending

Query Options

Name Type Description Default Optional
state boolean if true returns the Channel state true
watch boolean if true listen to changes to this Channel in real time. false
limit integer The number of channels to return (max is 30) 10
offset integer The offset (max is 1000) 0

Query channels allows you to retrieve channels and start watching them at same time using the watch parameter set to true.

Response

The result of querying a channel is a list of ChannelState objects which include all the required information to render them without any additional API call.

The event system ensures that your local copy of the channel state stays up to date.

Channel state is immutable. It leverages seamless-immutable to prevent bugs caused by accidentally editing state.

Paginating Channel Lists

Query channel requests can be paginated similar to how you paginate on other calls. Here's a short example:


// retrieve 20 channels with Thierry as a member and skip first 10

const filter = { members: { $in: ['thierry'] } };
const sort = { last_message_at: -1 };

const channels = await authClient.queryChannels(filter, sort, {
    limit: 20, offset: 10
});
                    

Channel Pagination

As shown in the code sample below, pagination for messages uses limit & id_lte parameters for pagination.

The term id_lte stands for ID less than or equal. The ID-based pagination improves performance and prevents issues related to the list of messages changing while you’re paginating.


const result = await channel.query({
    messages: { limit: 2, id_lte: 123 } ,
    members: { limit: 2, offset: 0 } ,
    watchers: { limit: 2, offset: 0 },
});
                    

For members and watchers, we use limit and offset parameters.


curl -i -X POST -d "{\"messages\":{\"limit\":2,\"id_lte\":123},\"members\":{\"limit\":2,\"offset\":0},\"watchers\":{\"limit\":2,\"offset\":0}}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.eoUjRuyJ1aUtTdZlZO9eX1lHkLXbUPWhdAb6Nr9GPNw" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso/query?api_key=c38bmrz86x8y"
                    

Updating a Channel

You can edit a Channel using the update method:


const response = await channel.update(
    {
        name: 'myspecialchannel',
        color: 'green',
    },
    { text: 'Thierry changed the channel color to green' },
);
                    

Request Params

Name

Type

Description

channel data

object

Object with the new channel information. One special field is frozen. If you set that to true new messages will be blocked.

message

object

Message object allowing you to show a system message in the Channel that something changed.


curl -i -X POST -d "{\"message\":{\"text\":\"Thierry changed the channel color to green\"},\"data\":{\"name\":\"myspecialchannel\",\"color\":\"green\"}}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.eoUjRuyJ1aUtTdZlZO9eX1lHkLXbUPWhdAb6Nr9GPNw" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"
                    

Changing Channel Members

Adding & Removing Channel Members

Using the addMembers() method adds the given users as members, while removeMembers() removes them.


await channel.addMembers(['thierry', 'josh']);
await channel.removeMembers(['tommaso']);
                    

# add thiery
curl -i -X POST -d "{\"add_members\":[\"thierry\"]}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.eoUjRuyJ1aUtTdZlZO9eX1lHkLXbUPWhdAb6Nr9GPNw" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"

# remove tommaso
curl -i -X POST -d "{\"remove_members\":[\"tommaso\"]}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.eoUjRuyJ1aUtTdZlZO9eX1lHkLXbUPWhdAb6Nr9GPNw" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"
                    
Note: You can only add/remove up to 100 members at once.

Adding & Removing Moderators to a Channel

Using the addModerators() method adds the given users as moderators (or updates their role to moderator if already members), while demoteModerators() removes the moderator status.


await channel.addModerators(['thierry', 'josh']);
await channel.demoteModerators(['tommaso']);
                    

channel.add_moderators(["thierry", "josh"]);
channel.demote_moderators(["tommaso"]);
                    

channel.add_moderators(["thierry", "josh"]);
channel.demote_moderators(["tommaso"]);
                    

$channel->addModerators(["thierry", "josh"]);
$channel->demoteModerators(["tommaso"]);
                    

// at the moment we don't have a Java client for server side usage
                    

hannel.AddModerators("thierry", "josh")
channel.DemoteModerators("tommaso")
                    

// at the moment we don't have a Swift client for server side usage
                    

await channel.AddModerators(new string[] { "thierry", "josh" });
await channel.DemoteModerators(new string[] { "tommaso" });
                    
These operations can only be performed server-side.

# add thierry
curl -i -X POST -d "{\"add_moderators\":[\"thierry\"]}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXJ2ZXJzaWRlIjp0cnVlfQ.kYvUM8XbLNWu6QAtD0yBmJI5tTXLiT_DC7qFaALDm_4" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"

# remove tommaso
curl -i -X POST -d "{\"demote_moderators\":[\"tommaso\"]}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXJ2ZXJzaWRlIjp0cnVlfQ.kYvUM8XbLNWu6QAtD0yBmJI5tTXLiT_DC7qFaALDm_4" \
    "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"
                    
You can only add/remove up to 100 moderators at once.

One to One Conversations

Creating Conversations

Channels can be used to create one to one conversations between users as well. In most cases, you want conversations to be unique and make sure that two users have only a channel.

You can achieve this by leaving the channel ID empty and provide channel type, members, and custom data. When you do so, the API will ensure that only one channel for the members you specified exists (the order of the members does not matter).

You cannot add/remove members for channels created this way.

const conversation = client.channel('messaging', '', {
    members: ['thierry', 'tommaso'],
});
                    

Invites

Inviting Users

Stream Chat provides the ability to invite users to a channel via the channel method with the invites array. Upon invitation, the end-user will receive a notification that they were invited to the specified channel.

See the following for an example on how to invite a user by adding an invites array containing the user ID:


const conversation = client.channel('messaging', 'awesome-chat', {
    name: 'Founder Chat',
    members: ['thierry', 'tommaso'],
    invites: ['nick'],
});

await conversation.create(); 
                    

Accepting an Invite

In order to accept an invite, you must use call the acceptInvite method. The acceptInvite method accepts and object with an optional message property. Please see below for an example of how to call acceptInvite:

The message can be used for system messages for display within the channel (e.g. "Nick joined this channel!").

// initialize the channel
const channel = client.channel('messaging', 'awesome-chat');

// accept the invite
await channel.acceptInvite({
    message: { text: 'Nick joined this channel!' },
});
                    

Rejecting an Invite

To reject an invite, call the rejectInvite method. This method does not require a user ID as it pulls the user ID from the current session in store from the setUser call.


await channel.rejectInvite();
                    

Query for Accepted Invites

Querying for accepted invites is done via the queryChannels method. This allows you to return a list of accepted invites with a single call. See below for an example:


const invites = client.queryChannels({
    invite: 'accepted',
});
                    

Query for Rejected Invites

Similar to querying for accepted invites, you can query for rejected invites with queryChannels. See below for an example:


const rejected = client.queryChannels({
    invite: 'rejected',
});
                    

Delete a Channel

You can delete a Channel using the delete method. This marks the channel as deleted and hides all the content.


const response = await channel.delete(); 
                    

curl -i -X DELETE
  -H "Content-Type: application/json" \
  -H "Stream-Auth-Type: jwt" \
  -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoidGhpZXJyeSJ9.eoUjRuyJ1aUtTdZlZO9eX1lHkLXbUPWhdAb6Nr9GPNw" \
  "https://chat-us-east-c1.stream-io-api.com/channels/messaging/thiery-tommaso?api_key=c38bmrz86x8y"
                    
If you recreate this channel, it will show up empty. Recovering old messages is not currently supported via the Stream Chat API.

Hiding Channels

Hiding a channel will remove it from query channel requests for that user until a new message is added.


// hides the channel until a new message is added there
await channel.hide();

// shows a previously hidden channel
await channel.show();
                    

Features

There are five built-in channel types:

  • livestream: Sensible defaults in case you want to build chat like YouTube or Twitch.
  • messaging: Configured for apps such as WhatsApp or Facebook Messenger.
  • gaming: Configured for in-game chat.
  • commerce: Good defaults for building something like your own version of Intercom or Drift.
  • team: If you want to build your own version of Slack or something similar, start here.

As you can see in the examples below, you can define your own Channel types and configure them to fit your needs. The Channel type allows you to configure these features:

  • typing_events: Controls if typing indicators are shown. Enabled by default.
  • read_events: Controls whether the chat shows how far you’ve read. Enabled by default.
  • connect_events: Determines if events are fired for connecting and disconnecting to a chat. Enabled by default.
  • search: Controls if messages should be searchable (this is a premium feature). Disabled by default.
  • reactions: If users are allowed to add reactions to messages. Enabled by default.
  • replies: Enables message threads and replies. Enabled by default.
  • mutes: Determines if users are able to mute other users. Enabled by default.
  • uploads: Allows image and file uploads within messages. Enabled by default.
  • url_enrichment: When enabled, messages containing URLs will be enriched automatically with image and text related to the message. Enabled by default.

It allows you to specify these settings:

Name Type Description Default Optional
automod string Disabled, simple or AI are valid options for the Automod (AI based moderation is a premium feature) simple
message_retention string A number of days or infinite infinite
max_message_length int The max message length 5,000
default default default default
You need to use server-side authentication to create, edit, or delete a channel type.

Creating a Channel Type


await client.createChannelType({
    name: 'public', 
    permissions:[
        new Permission(
            name: 'Allow reads for all',
            priority: 999,
            resources: ['ReadChannel', 'CreateMessage'],
            AnyRole,
        ),
        DenyAll
    ],
    mutes: false,
    reactions: false
});
                    

client.create_channel_type({
    "name": "public",
    "permissions": [
        {
            "name": "Allow reads for all",
            "priority": 999,
            "resources": ["ReadChannel", "CreateMessage"],
            "action": "Allow",
        },
        {"name": "Deny all", "priority": 1, "resources": ["*"], "action": "Deny"},
    ],
    "mutes": False,
    "reactions": False,
})
                    

client.create_channel_type({
    'name' => 'public',
    'permissions' => [
	{
	    'name' => 'Allow reads for all',
            'priority' => 999,
	    'resources' => ['ReadChannel', 'CreateMessage'],
	    'action' => 'Allow',
	},
	{'name' => 'Deny all', 'priority' => 1, 'resources' => ['*'], 'action' => 'Deny'}
    ],
    'mutes' => false,
    'reactions' => false
})
                    

$channelConf = [
    'name' => 'public',
    'permissions' => [
	{
	    'name' => 'Allow reads for all',
            'priority' => 999,
	    'resources' => ['ReadChannel', 'CreateMessage'],
	    'action' => 'Allow',
	},
	{'name' => 'Deny all', 'priority' => 1, 'resources' => ['*'], 'action' => 'Deny'}
    ],
    'mutes' => false,
    'reactions' => false
];

$client->createChannelType($channelConf);
                    

// at the moment we don't have a Java client for server side usage
                    

// TODO
                    

// at the moment we don't have a Swift client for server side usage
                    

var c = new ChannelTypeInput()
{
    Name = "livechat",
    Automod = Autmod.Disabled,
    Commands = new List() { Commands.Ban },
    Mutes = true
};

await client.CreateChannelType(c);
                    

If not provided, the permission policies will default to the ones from the built-in team type.

List Channel Types

You can retrieve the list of all channel types defined for your application.


await client.listChannelTypes();
                    

Get a Channel Type

You can retrieve a channel type definition with this endpoint. Note: features and commands are also returned by other channel endpoints.


client.getChannelType('public');
                    

Edit a Channel Type

Channel type features, commands and permissions can be changed. Only the fields that must change need to be provided, fields that are not provided to this API will remain unchanged.


client.updateChannelType('public', {
    permissions: [AllowAll, DenyAll],
    replies: false,
    commands: ["all"]
});
                    


curl -i -X PUT -d "{\"permissions\": [\"AllowAll\", \"DenyAll\"], \"replies\": false}"\
    -H "Content-Type: application/json" \
    -H "Stream-Auth-Type: jwt" \
    -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXJ2ZXJzaWRlIjp0cnVlfQ.EcVae7nDuSvG6AXIBXSHPSHspQMN0BgsqE6LGfr2zpc" \
    "https://chat-us-east-c1.stream-io-api.com/channeltypes/public?api_key=gxuz347utvag"
                    

Features of a channel can be updated by passing the boolean flags:


client.updateChannelType("public", {
    typing_events: false,
    read_events: true,
    connect_events: true,
    search: false,
    reactions: true,
    replies: false,
    mutes: true
});
                    

Settings can also be updated by passing in the desired new values:


client.updateChannelType("public", {
    automod: "disabled",
    message_retention: "7",
    max_message_length: 140,
    commands: ["imgur", "giphy", "ban"]
});
                    

Permissions for a channel type can be updated either by using predefined values or creating new ones:


client.updateChannelType("public", {
    permissions: [
        new Permission(
            name: 'Allow reads for all',
            priority: 999,
            resources: ['ReadChannel', 'CreateMessage'],
            AnyRole
        ),
        DenyAll
    ]
});
                    

Remove a Channel Type


client.deleteChannelType('public');
                    
You cannot delete a channel type if there are any active channels of that type.

Permissions

The channel type also allows you to specify the permissions your chat can use. If not provided, the permissions will default to team’s channel type permissions.

The Channel object allows you to define a list of moderators and admins. The user object allows you to mark a user as a moderator or admin for your entire application.

Note that you can only modify these fields via the backend API:


// editing a channel (must be done server-side)
const channel = serverClient.channel('organization', 'spacex').update({
    image: image,
    created_by: elon,
    roles: { elon: 'admin', gwynne: 'moderator' },
});
                    

data = {
    "image": "https://path/to/image",
    "created_by": "elon",
    "roles": {"elon": "admin", "gwynne": "moderator"},
}

spacex_channel = client.channel("team", "spacex")
spacex_channel.update(data)
                    

You can edit a user like this:


await serverClient.updateUser({
    id: 'tommaso',
    name: 'Tommaso Barbugli',
    role: 'admin',
});
                    

client.update_user({"id": "tommaso", "name": "Tommaso", "role": "admin"})
                    

Server-side, you’re allowed to do everything. For client-side integrations, the permission system kicks in. This system specifies a list of permissions the given user is allowed to do.

Changing user roles is only allowed server-side.

Permission checking is performed taking into account parameters:

  • API Request: the action the user is performing (e.g. send a message, edit a message, etc.)
  • User Role
  • Channel Type
  • Resource: the resource involved in the API call (e.g. a channel, a message, a user, etc.)
  • Ownership: whether or not the resource is owned by the user (when applicable)

Channel Type

Most API requests are specific to a channel (e.g. add a message to channel “livestream:rockets”). Channel types have different permissions and can be configured via APIs. The five default channel types come with good default permission policies. You can find more information on how to manage permissions in the Channel Types section.

User Role & User Channel Role

  1. admin: This role allows users to perform more advanced actions. This role should be granted only to staff users.
  2. moderator: Allows users to perform moderation (e.g. ban users, add/remove users, …).
  3. user: This is the default role assigned to any user.
  4. channel_member: A user that is also listed as a member on a channel.
  5. guest: Users added using 
  6. anonymous: Users added using
A user can have the moderator role for some specific channels in addition to the user role for all other channels.

Object Ownership

If applicable, ownership of the entity is taken into account. This parameter allows you to grant users the ability to edit their own messages while denying editing others’ messages. Permission policies are organized as list ordered by priority. A permission policy has the following fields:

Channel Type

The channel the policy applies to.

Name

The unique name for the policy (eg. "Channel member permissions").

Resources

The list of API resources the policy applies to. Policies can match one more resources by their name (see full list below) or any resource by using the wildcard resource value “*”.

Roles

The list of roles the policy applies to. This value can be empty in the case the user role is not going to be taken into account.

Owner

Whether the policy should be applied to requested that alter an object owned by the requesting user. This field is either true or false.

Action

The action to apply to the API request once resources, roles, and action fields are matching. The two allowed values are: Allow and Deny.

Priority

The priority of the policy. Policies are evaluated ordered by their priority, this field allows you to create a stable order.

Default Permission Policies

The five built-in channel types ship with default permission setting.

Messaging

Admin

Moderator

User

Guest

Anonymous

Channel Member

Owner

Create Channel

Yes

Yes

Yes

No

No

Yes

-

Read Channel

Yes

Yes

No

No

No

Yes

-

Update Channel Members

Yes

Yes

No

No

No

No

Yes

Update Channel Roles

No

No

No

No

No

No

-

Update Channel

Yes

Yes

No

No

No

No

Yes

Create Message

Yes

Yes

No

No

No

No

-

Update Message

Yes

Yes

No

No

No

No

Yes

Delete Message

Yes

Yes

No

No

No

No

Yes

Ban User

Yes

Yes

No

No

No

No

-

Edit User Role

No

No

No

No

No

No

-

Edit User

No

No

No

No

No

No

Yes

Upload Attachment

Yes

Yes

No

No

No

Yes

-

Delete Attachment

Yes

Yes

No

No

No

No

Yes

Use Commands

Yes

Yes

No

No

No

Yes

-

Add Links

Yes

Yes

No

No

No

Yes

-

Livestream

Admin

Moderator

User

Guest

Anonymous

Channel Member

Owner

Create Channel

Yes

Yes

Yes

No

No

Yes

-

Read Channel

Yes

Yes

Yes

Yes

Yes

Yes

-

Update Channel Members

Yes

No

No

No

No

No

-

Update Channel Roles

Yes

No

No

No

No

No

-

Update Channel

Yes

No

No

No

No

No

-

Create Message

Yes

Yes

Yes

No

No

Yes

-

Update Message

Yes

Yes

No

No

No

No

-

Delete Message

Yes

Yes

No

No

No

No

Yes

Ban User

Yes

Yes

No

No

No

No

-

Edit User Role

No

No

No

No

No

No

-

Edit User

No

No

No

No

No

No

Yes

Upload Attachment

Yes

Yes

Yes

No

No

Yes

-

Delete Attachment

Yes

Yes

No

No

No

No

Yes

Use Commands

Yes

Yes

Yes

No

No

Yes

-

Add Links

Yes

Yes

Yes

No

No

Yes

-

Gaming

Admin

Moderator

User

Guest

Anonymous

Channel Member

Owner

Create Channel

Yes

No

No

No

No

No

-

Read Channel

Yes

Yes

No

No

No

Yes

Yes

Update Channel Members

Yes

No

No

No

No

No

-

Update Channel Roles

Yes

No

No

No

No

No

-

Update Channel

Yes

No

No

No

No

No

-

Create Message

Yes

Yes

No

No

No

Yes

-

Update Message

Yes

Yes

No

No

No

No

Yes

Delete Message

Yes

Yes

No

No

No

No

Yes

Ban User

Yes

Yes

No

No

No

No

-

Edit User Role

Yes

No

No

No

No

No

-

Edit User

Yes

No

No

No

No

No

Yes

Upload Attachment

Yes

Yes

No

No

No

Yes

-

Delete Attachment

Yes

Yes

No

No

No

No

Yes

Use Commands

Yes

Yes

No

No

No

Yes

-

Add Links

Yes

Yes

No

No

No

Yes

-

Commerce

Admin

Moderator

User

Guest

Anonymous

Channel Member

Owner

Create Channel

Yes

Yes

No

Yes

No

Yes

-

Read Channel

Yes

Yes

No

No

No

Yes

Yes

Update Channel Members

Yes

Yes

No

No

No

No

Yes

Update Channel Roles

No

No

No

No

No

No

-

Update Channel

Yes

Yes

No

No

No

No

-

Create Message

Yes

Yes

No

No

No

Yes

-

Update Message

Yes

Yes

No

No

No

No

Yes

Delete Message

Yes

Yes

No

No

No

No

Yes

Ban User

Yes

Yes

No

No

No

No

-

Edit User Role

No

No

No

No

No

No

-

Edit User

Yes

Yes

No

No

No

No

Yes

Upload Attachment

Yes

Yes

No

Yes

No

Yes

-

Delete Attachment

Yes

Yes

No

No

No

No

Yes

Use Commands

Yes

Yes

No

No

No

No

-

Add Links

Yes

Yes

No

Yes

No

Yes

-

Team

Admin

Moderator

User

Guest

Anonymous

Channel Member

Owner

Create Channel

Yes

Yes

Yes

No

No

Yes

-

Read Channel

Yes

Yes

No

No

No

Yes

Yes

Update Channel Members

Yes

Yes

No

No

No

No

Yes

Update Channel Roles

No

No

No

No

No

No

-

Update Channel

Yes

Yes

No

No

No

No

Yes

Create Message

Yes

Yes

No

No

No

Yes

-

Update Message

Yes

Yes

No

No

No

No

Yes

Delete Message

Yes

Yes

No

No

No

No

Yes

Ban User

Yes

Yes

No

No

No

No

-

Edit User Role

No

No

No

No

No

No

-

Edit User

No

No

No

No

No

No

Yes

Upload Attachment

Yes

Yes

No

No

No

Yes

-

Delete Attachment

Yes

Yes

No

No

No

No

Yes

Use Commands

Yes

Yes

No

No

No

Yes

-

Add Links

Yes

Yes

No

No

No

Yes

-

Data Format

User presence allows you to show when a user was last active and if they are online right now. Whenever you read a user the data will look like this:


{
    id: 'unique_user_id',
    online: true,
    status: 'Eating a veggie burger...',
    last_active: '2019-01-07T13:17:42.375Z'
}
                    
The online field indicates if the user is online. The status field stores text indicating the current user status.

Invisible

To mark a user invisible simply set the invisible property to true. You can also set a custom status message at the same time:


// mark a user as invisible
await client.setUser({
    id: 'unique_user_id',
    invisible: true
});
                    
When invisible is set to true, the current user will appear as offline to other users.

Listening to Presence Changes

Of course, you want to listen to the user presence changes. This allows you to show a user as offline when they leave and update their status in real time. These 3 endpoints allow you to watch user presence:


// If you pass presence: true to channel.watch it will watch the list of user presence changes.
// Note that you can listen to at most 10 users using this API call
const channel = client.channel('messaging', 'my-conversation-123', {
    members: ['john', 'jack'],
    color: 'green'
});

const state = await channel.watch({ presence: true });

// queryChannels allows you to listen to the members of the channels that are returned
// so this does the same thing as above and listens to online status changes for john and jack
const channels = await client.queryChannels(
    { color: 'green' },
    { last_message_at: -1 },
    { presence: true }
);

// queryUsers allows you to listen to user presence changes for john and jack
const users = await client.queryUsers({
    id: {
      $in: [
        'john', 
        'jack'
       ]
    }}, 
    { id: -1 }, 
    { presence: true }
 );
                    

A users online status change can be handled via event delegation by subscribing to the  user.presence.changed event the same you do for any other event.

Unread

The most common use case for client level events are unread counts. Here's an example of a complete unread count integration for your chat app. As a first step we get the unread count when the user connects:


const response = await client.setUser({ id: 'myid' }, token);

// response.me.total_unread_count returns the unread count
// response.me.unread_channels returns the count of channels with unread messages
                    

By default the React components will mark the messages as read automatically. You can also make the call manually like this:


// mark all messages on a channel as read
await channel.markRead()
                    

While you're using the app, the unread count can change. A user can be added to a channel, a new message can be created, or the user can mark the messages as seen on another tab/device.

The markRead function can also be executed server-side by passing a user ID as shown in the example below:


// mark all messages on a channel as read (server side)
await channel.markRead({ user_id: 'foo' })
                    

To support updating the unread count in realtime, you can listen to these events.

  1. notification.added_to_channel

  2. notification.removed_from_channel

  3. notification.message_new

  4. notification.mark_read

All 4 of these events include the fields: total_unread_count and unread_channels. You can listen to them all at once like this:


client.on((event) => {
     if (event.total_unread_count !== undefined) {
         console.log(event.total_unread_count);
     }

     if (event.unread_channels !== undefined) {
         console.log(event.unread_channels);
     }
});
                    

Channels

When you retrieve a channel from the API (e.g. using query channels), the read state for all members is included in the response. This allows you to display which messages are read by each user. For each member, we include the last time he or she marked the channel as read.


const channel = client.channel('messaging', 'test');
await channel.watch();

console.log(channel.state.read);

//{ '2fe6019c-872f-482a-989e-ecf4f786501b':
//  { user: 
//    { 
//      id: '2fe6019c-872f-482a-989e-ecf4f786501b',
//      role: 'user',
//      created_at: '2019-04-24T13:09:19.664378Z',
//      updated_at: '2019-04-24T13:09:23.784642Z',
//      last_active: '2019-04-24T13:09:23.781641Z',
//      online: true
//    },
//    last_read: 2019-04-24T13:09:21.623Z
//  }
//}
                    

Unread messages per channel

You can retrieve the count of unread messages for the current user on a channel like this:


channel.countUnread();
                    

Unread mentions per channel

You can retrieve the count of unread messages mentioning the current user on a channel like this:


channel.countUnreadMentions();
                    

Mark all as read

You can mark all channels as read for a user like this:


// client-side
await client.markAllRead();

// mark all as read for one user server-side 
await serverSideClient.markAllRead({ user:  { id: 'myid' } });
                    

Tools

Flag

Any user is allowed to flag a message or a user.


const data = await client.flagMessage(messageResponse.message.id);
                    

Mute

Any user is allowed to mute another user.


// client-side mute
const data = await client.muteUser('eviluser');

// server-side mute requires the id of the user creating the mute as well
const data = await client.muteUser('eviluser', 'user_id');
                    

Ban

Users can be banned from an app entirely or from a channel. When a user is banned, it will be not allowed to post messages until the ban is removed or expired. In most cases only admins or moderators are allowed to ban other users from a channel.

Name Type Description Default Optional
timeout number The timeout in minutes until the ban is automatically expired. no limit
reason string The reason that the ban was created.
Banning a user from all channels can only be done using server-side auth.

// ban a user for 60 minutes from all channel
let data = await client.banUser('eviluser', {
    timeout: 60,
    reason: 'Banned for one hour',
});

// ban a user from the livestream:fortnite channel
data = await channel.banUser('eviluser', {
    reason: 'Profanity is not allowed here',
});

// remove ban from channel
data = await channel.unbanUser('eviluser');

// remove global ban
data = await authClient.unbanUser('eviluser');
                    

# ban a user for 60 minutes from all channel
client.ban_user("eviluser", timeout=60, reason="Banned for one hour")

# ban a user from the livestream:fortnite channel
channel.ban_user("eviluser", reason="Profanity is not allowed here")

# remove ban from channel
channel.unban_user("eviluser")

# remove global ban
client.unban_user("eviluser")
                    

client.banUser(userId, channel, reason, timeout, new CompletableCallback() {
    @Override
    public void onSuccess(CompletableResponse response) {
        
    }

    @Override
    public void onError(String errMsg, int errCode) {

    }
});
                    

channel.ban(user: user, timeoutInMinutes: timeout, reason: reason)
    .subscribe()
    .disposed(by: disposeBag)
                    

client.banUser(userId, channel, reason, timeout, object : CompletableCallback {
    override fun onSuccess(response: CompletableResponse) {

    }

    override fun onError(errMsg: String, errCode: Int) {

    }
})
                    

Automod

For livestream and gaming channel types Automod is enabled by default. There are three options for moderation:

  • disabled
  • simple (blocks messages that contain a list of profane words)
  • ai (uses an AI-based classification system to detect various types of bad content)

You can configure the thresholds for the AI-based system by specifying a value between 0 and 1 for the following types of bad content. As an example, if you set the value for Spam to 0.7 it will block any message that seems very likely to be spam. On the other hand, if you set a value of 0.3 it will be very sensitive and block anything that remotely looks like spam.

  • Spam: Unsolicited messages – typically used as clickbait for financial gain from bot messages.
  • Toxic: Not a constructive comment, or in general not nice. E.g. "This is dumb" would be classified highly as toxic and only slightly obscene.
  • SevereToxic: Worst of the worst. Typically insulting, obscene, and/or degrading. E.g. "What a mother******* piece of crap those ****heads are for blocking us!".
  • Obscene: Generally reserved for inappropriate words. Typically offensive to accepted standards of morality.
  • Threat: A direct threat to another user. E.g. "I will spit on you" would classified highly as toxic and a threat but only slightly obscene.
  • Insult: Insulting another user. E.g. "You are dumb" would be classified as toxic and insulting, yet only moderately obscene.
  • IdentityHate: Degradation of a protected class.
AI-based moderation is a premium feature. Please contact sales to discuss your options.

Quickstart

Stream's command line interface (CLI) makes it easy to create and manage your Stream Chat applications directly from the command line. The source code and full documentation can be found on GitHub.

Installation

The Stream CLI must be installed globally using yarn or npm. Yarn is preferred; however, npm is fully supported. Yarn can be installed on macOS using Homebrew with the brew install yarn command and npm comes bundled with Node.js. We recommend running the latest version of Node.js when using the Stream CLI.


// yarn
yarn global add getstream-cli

// npm
npm install -g getstream-cli
                    

Initialization

Once installed, the stream command will be available globally within your command line. To get started with the Stream's, you must first initialize the CLI using the stream config:set command. This will ask you a series of questions that are required for the CLI to work properly. Please see below for an example.


$ stream config:set

✔ What is your full name? · Nick Parsons
✔ What is your email address associated with Stream? · nick@getstream.io
✔ What is your Stream API key? · 12345
✔ What is your Stream API secret? · *************************************
✔ What is your Stream API base URL? · https://chat-us-east-1.stream-io-api.com
✔ What environment would you like to run in? · production
✔ Would you like to enable error tracking for debugging purposes? · true

Your Stream CLI configuration has been generated! 🚀
                    

Documentation

The CLI is fully self documented and can be found on GitHub. Further supported commands can be found within the CLI documentation.

Auth

Enable Development Tokens

Token validation can be disabled for development apps. This allows you to use development tokens and work on user token provisioning later on.


// disable auth checks, allows dev token usage
await client.updateAppSettings({
    disable_auth_checks: true,
});

// re-enable auth checks
await client.updateAppSettings({
    disable_auth_checks: false,
});
                    

# disable auth checks, allows dev token usage
client.update_app_settings(disable_auth_checks=True)

# re-enable auth checks
client.update_app_settings(disable_auth_checks=False)
                    

Client.shared.set(user: user, token: .development)
                    

Disable Permission Checking

By default all apps ship with role based permission checks. During development you can decide to turn off permission checks, this way all users will act as admin users.


// disable permission checks
await client.updateAppSettings({
	disable_permissions_checks: true,
});

// re-enable permission checks
await client.updateAppSettings({
	disable_permissions_checks: false,
});
                    

# disable permission checks
client.update_app_settings(disable_permissions_checks=True)

# re-enable permission checks
client.update_app_settings(disable_permissions_checks=False)
                    

Webhooks Setup

By using webhooks, you can receive all events within your application. When configured, every event happening on Stream Chat will propagate to your webhook endpoint via an HTTP POST request.

Webhooks can help you migrate from a different chat provider to Stream without disruption, or to support complex notification mechanisms (e.g. sending an SMS to an offline user when a direct message is sent, etc.).

Webhook Requirements

In order to use webhooks, the endpoint responding to the webhook event must:

  • Be reachable from public internet, tunneling services like Ngrok are supported
  • Respond with a 200 HTTP code in less than 3 seconds
  • Handle HTTP requests with POST body
  • Able to parse JSON payloads
  • Support HTTP/1.1

While not required, we recommend following these best-practices for production environments:

  • Use SSL over HTTP using a certificate from a trusted authority (eg. Let's Encrypt)
  • Verify the "x-signature" header
  • Support Keep-Alive
  • Be highly available
  • Offload the processing of the message (read, store, and forget)

Configuration via CLI


stream chat:push:webhook --url 'https://acme.com/my/awesome/webhook/handler/'
                    
See the CLI introduction for more information on the Stream CLI.

Verify Events via X-Signature

All HTTP requests can be verified as coming from Stream (and not tampered by a 3rd party) by analyzing the signature attached to the request. Every request includes an HTTP header called "x-signature" containing a cryptographic signature of the message. Your webhook endpoint can validate that payload and signature match.


// first argument is the request body as a string, second the signature header
const isValid = client.verifyWebhook(req.rawBody, req.headers['x-signature']);
                    

import stream_chat

client = stream_chat.connect('API_KEY', 'API_SECRET')

# Django request
valid = client.verify_webhook(request.body, request.META['HTTP_X_SIGNATURE'])

# Flask request
valid = client.verify_webhook(request.data, request.headers['X-SIGNATURE'])
                    

require 'stream-chat'

client = StreamChat::Client.new(api_key='STREAM_KEY', api_secret='STREAM_SECRET')

// signature comes from the HTTP header x-signature
valid = client.verify_webhook(request_body, signature)
                    

$client = new GetStream\StreamChat\Client("STREAM_API_KEY", "STREAM_API_SECRET");

// signature comes from the HTTP header x-signature
$valid = $client->verifyWebhook($requestBody, $signature);
                    

// at the moment we don't have a Java client for server side usage
                    

client, _ := stream.NewClient(APIKey, []byte(APISecret))

// signature comes from the HTTP header x-signature
isValid := client.VerifyWebhook(body, signature)
                    

// at the moment we don't have a Swift client for server side usage
                    

using StreamChat;

var client = new Client("API KEY", "API SECRET");

// signature comes from the HTTP header x-signature 
client.VerifyWebhook(requestBody, signature);
                    

Events

Below you can find the complete list of events that are sent via webhooks together with the description of the data payload.

For message and channel events the webhook request body will also include the list of channel members and attach additional information about their read status. For performance reasons, such list is only included for channels with up to 50 members.

When applicable, the following attributes are included to the event user and to the event members:

total_unread_count

the total count of messages across all channels.

unread_channels

the count of channels with at least one unread message.

channel_last_read_at

the last time the channel was marked as read.

channel_unread_count

the count of unread messages on this channel

Webhook Event Types

Event

TRiggered

message.new

when a new message is added.

message.read

when a user calls mark as read.

message.updated

when a message is updated.

message.deleted

when a message is deleted.

reaction.new

when a message reaction is added.

reaction.deleted

when a message reaction deleted

member.added

when a member is added to a channel.

member.updated

when a member is updated.

member.removed

when a member is removed from a channel.

channel.updated

when a channel is updated.

channel.deleted

when a channel is deleted.

user.updated

when a user is updated.

user.muted

when a user is muted.

user.unmuted

when a user is unmuted.

channel.truncated

when a channel is truncated.

message.new


{
  "cid": "messaging:fun",
  "type": "message.new",
  "message": {
    "id": "fff0d7c0-60bd-4835-833b-3843007817bf",
    "text": "8b780762-4830-4e2a-aa43-18aabaf1732d",
    "html": "

8b780762-4830-4e2a-aa43-18aabaf1732d

\n", "type": "regular", "user": { "id": "97b49906-0b98-463b-aa47-0aa945677eb2", "role": "user", "created_at": "2019-04-24T08:48:38.440123Z", "updated_at": "2019-04-24T08:48:38.440708Z", "online": false }, "attachments": [], "latest_reactions": [], "own_reactions": [], "reaction_counts": null, "reply_count": 0, "created_at": "2019-04-24T08:48:39.918761Z", "updated_at": "2019-04-24T08:48:39.918761Z", "mentioned_users": [] }, "user": { "id": "97b49906-0b98-463b-aa47-0aa945677eb2", "role": "user", "created_at": "2019-04-24T08:48:38.440123Z", "updated_at": "2019-04-24T08:48:38.440708Z", "online": false, "channel_unread_count": 1, "channel_last_read_at": "2019-04-24T08:48:39.900585Z", "total_unread_count": 1, "unread_channels": 1, "unread_count": 1 }, "created_at": "2019-04-24T08:48:38.949986Z", "members": [ { "user_id": "97b49906-0b98-463b-aa47-0aa945677eb2", "user": { "id": "97b49906-0b98-463b-aa47-0aa945677eb2", "role": "user", "created_at": "2019-04-24T08:48:38.440123Z", "updated_at": "2019-04-24T08:48:38.440708Z", "online": false, "channel_unread_count": 1, "channel_last_read_at": "2019-04-24T08:48:39.900585Z", "total_unread_count": 1, "unread_channels": 1, "unread_count": 1 }, "created_at": "2019-04-24T08:48:39.652296Z", "updated_at": "2019-04-24T08:48:39.652296Z" } ] }

message.read


{
  "cid": "messaging:fun",
  "type": "message.read",
  "user": {
    "id": "a6e21b36-798b-408a-9cd1-0cf6c372fc7f",
    "role": "user",
    "created_at": "2019-04-24T08:49:58.170034Z",
    "updated_at": "2019-04-24T08:49:59.345304Z",
    "last_active": "2019-04-24T08:49:59.344201Z",
    "online": true,
    "total_unread_count": 0,
    "unread_channels": 0,
    "unread_count": 0,
    "channel_unread_count": 0,
    "channel_last_read_at": "2019-04-24T08:49:59.365498Z"
  },
  "created_at": "2019-04-24T08:49:59.365489Z"
}
                    

message.updated


{
  "cid": "messaging:fun",
  "type": "message.updated",
  "message": {
    "id": "93163f53-4174-4be8-90cd-e59bef78da00",
    "text": "new stuff",
    "html": "

new stuff

\n", "type": "regular", "user": { "id": "75af03a7-fe83-4a2a-a447-9ed4fac2ea36", "role": "user", "created_at": "2019-04-24T08:51:26.846395Z", "updated_at": "2019-04-24T08:51:27.973941Z", "last_active": "2019-04-24T08:51:27.972713Z", "online": false }, "attachments": [], "latest_reactions": [], "own_reactions": [], "reaction_counts": null, "reply_count": 0, "created_at": "2019-04-24T08:51:28.005691Z", "updated_at": "2019-04-24T08:51:28.138422Z", "mentioned_users": [] }, "user": { "id": "75af03a7-fe83-4a2a-a447-9ed4fac2ea36", "role": "user", "created_at": "2019-04-24T08:51:26.846395Z", "updated_at": "2019-04-24T08:51:27.973941Z", "last_active": "2019-04-24T08:51:27.972713Z", "online": true, "channel_unread_count": 1, "channel_last_read_at": "2019-04-24T08:51:27.994245Z", "total_unread_count": 2, "unread_channels": 2, "unread_count": 2 }, "created_at": "2019-04-24T10:51:28.142291+02:00" }

message.deleted


{
  "cid": "messaging:fun",
  "type": "message.deleted",
  "message": {
    "id": "268d121f-82e0-4de1-8c8b-ef1201efd7a3",
    "text": "new stuff",
    "html": "

new stuff

\n", "type": "regular", "user": { "id": "76cd8430-2f91-4059-90e5-02dffb910297", "role": "user", "created_at": "2019-04-24T09:44:21.390868Z", "updated_at": "2019-04-24T09:44:22.537305Z", "last_active": "2019-04-24T09:44:22.535872Z", "online": false }, "attachments": [], "latest_reactions": [], "own_reactions": [], "reaction_counts": {}, "reply_count": 0, "created_at": "2019-04-24T09:44:22.57073Z", "updated_at": "2019-04-24T09:44:22.717078Z", "deleted_at": "2019-04-24T09:44:22.730524Z", "mentioned_users": [] }, "created_at": "2019-04-24T09:44:22.733305Z" }

reaction.new


{
  "cid": "messaging:fun",
  "type": "reaction.new",
  "message": {
    "id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6",
    "text": "new stuff",
    "html": "

new stuff

\n", "type": "regular", "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T09:49:48.300566Z", "online": false }, "attachments": [], "latest_reactions": [ { "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T09:49:48.300566Z", "online": true }, "type": "lol", "created_at": "2019-04-24T09:49:48.481994Z" } ], "own_reactions": [], "reaction_counts": { "lol": 1 }, "reply_count": 0, "created_at": "2019-04-24T09:49:48.334808Z", "updated_at": "2019-04-24T09:49:48.483028Z", "mentioned_users": [] }, "reaction": { "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T09:49:48.300566Z", "online": true }, "type": "lol", "created_at": "2019-04-24T09:49:48.481994Z" }, "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T09:49:48.300566Z", "online": true, "unread_channels": 2, "unread_count": 2, "channel_unread_count": 1, "channel_last_read_at": "2019-04-24T09:49:48.321138Z", "total_unread_count": 2 }, "created_at": "2019-04-24T09:49:48.488497Z" }

reaction.deleted


{
  "cid": "messaging:fun",
  "type": "reaction.deleted",
  "message": {
    "id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6",
    "text": "new stuff",
    "html": "

new stuff

\n", "type": "regular", "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T09:49:48.300566Z", "online": false }, "attachments": [], "latest_reactions": [], "own_reactions": [], "reaction_counts": {}, "reply_count": 0, "created_at": "2019-04-24T09:49:48.334808Z", "updated_at": "2019-04-24T09:49:48.511631Z", "mentioned_users": [] }, "reaction": { "message_id": "4b3c7b6c-a39d-4069-9450-2a3716cf4ca6", "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T11:49:48.497656+02:00", "online": true }, "type": "lol", "created_at": "2019-04-24T09:49:48.481994Z" }, "user": { "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3", "role": "user", "created_at": "2019-04-24T09:49:47.158005Z", "updated_at": "2019-04-24T09:49:48.301933Z", "last_active": "2019-04-24T11:49:48.497656+02:00", "online": true, "total_unread_count": 2, "unread_channels": 2, "unread_count": 2, "channel_unread_count": 1, "channel_last_read_at": "2019-04-24T09:49:48.321138Z" }, "created_at": "2019-04-24T09:49:48.511082Z" }

member.added


{
  "cid": "messaging:fun",
  "type": "member.added",
  "member": {
    "user_id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
    "user": {
      "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
      "role": "user",
      "created_at": "2019-04-24T09:49:47.149933Z",
      "updated_at": "2019-04-24T09:49:47.151159Z",
      "online": false
    },
    "created_at": "2019-04-24T09:49:48.534412Z",
    "updated_at": "2019-04-24T09:49:48.534412Z"
  },
  "user": {
    "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
    "role": "user",
    "created_at": "2019-04-24T09:49:47.149933Z",
    "updated_at": "2019-04-24T09:49:47.151159Z",
    "online": false,
    "channel_last_read_at": "2019-04-24T09:49:48.537084Z",
    "total_unread_count": 0,
    "unread_channels": 0,
    "unread_count": 0,
    "channel_unread_count": 0
  },
  "created_at": "2019-04-24T09:49:48.537082Z"
}
                    

member.updated


{
  "cid": "messaging:fun",
  "type": "member.updated",
  "member": {
    "user_id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
    "user": {
      "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
      "role": "user",
      "created_at": "2019-04-24T09:49:47.149933Z",
      "updated_at": "2019-04-24T09:49:47.151159Z",
      "online": false
    },
    "is_moderator": true,
    "created_at": "2019-04-24T09:49:48.534412Z",
    "updated_at": "2019-04-24T09:49:48.547034Z"
  },
  "user": {
    "id": "d4d7b21a-78d4-4148-9830-eb2d3b99c1ec",
    "role": "user",
    "created_at": "2019-04-24T09:49:47.149933Z",
    "updated_at": "2019-04-24T09:49:47.151159Z",
    "online": false,
    "total_unread_count": 0,
    "unread_channels": 0,
    "unread_count": 0,
    "channel_unread_count": 0,
    "channel_last_read_at": "2019-04-24T09:49:48.549211Z"
  },
  "created_at": "2019-04-24T09:49:48.54921Z"
}
                    

member.removed


{
    "cid": "messaging:fun",
    "type": "member.removed",
    "user": {
        "id": "6585dbbb-3d46-4943-9b14-a645aca11df4",
        "role": "user",
        "created_at": "2019-03-22T14:22:04.581208Z",
        "online": false
    },
    "created_at": "2019-03-22T14:22:07.040496Z"
}
                    

channel.updated


{
    "cid": "messaging:fun",
    "type": "channel.updated",
    "channel": {
        "cid": "messaging:fun",
        "id": "fun",
        "type": "messaging",
        "last_message_at": "2019-04-24T09:49:48.576202Z",
        "created_by": {
            "id": "57fabaed-446a-40b4-a6ec-e0ac8cad57e3",
            "role": "user",
            "created_at": "2019-04-24T09:49:47.158005Z",
            "updated_at": "2019-04-24T09:49:48.301933Z",
            "last_active": "2019-04-24T09:49:48.497656Z",
            "online": true
        },
        "created_at": "2019-04-24T09:49:48.180908Z",
        "updated_at": "2019-04-24T09:49:48.180908Z",
        "frozen": false,
        "config": {
            "created_at": "2016-08-18T16:42:30.586808Z",
            "updated_at": "2016-08-18T16:42:30.586808Z",
            "name": "messaging",
            "typing_events": true,
            "read_events": true,
            "connect_events": true,
            "search": true,
            "reactions": true,
            "replies": true,
            "mutes": true,
            "message_retention": "infinite",
            "max_message_length": 5000,
            "automod": "disabled",
            "commands": [
                "giphy",
                "flag",
                "ban",
                "unban",
                "mute",
                "unmute"
            ]
        },
        "awesome": "yes"
    },
    "created_at": "2019-04-24T09:49:48.594316Z"
}
                    

channel.deleted


{
  "cid": "messaging:fun",
  "type": "channel.deleted",
  "channel": {
    "cid": "messaging:fun",
    "id": "fun",
    "type": "messaging",
    "created_at": "2019-04-24T09:49:48.180908Z",
    "updated_at": "2019-04-24T09:49:48.180908Z",
    "deleted_at": "2019-04-24T09:49:48.626704Z",
    "frozen": false,
    "config": {
      "created_at": "2016-08-18T18:42:30.586808+02:00",
      "updated_at": "2016-08-18T18:42:30.586808+02:00",
      "name": "messaging",
      "typing_events": true,
      "read_events": true,
      "connect_events": true,
      "search": true,
      "reactions": true,
      "replies": true,
      "mutes": true,
      "message_retention": "infinite",
      "max_message_length": 5000,
      "automod": "disabled",
      "commands": [
        "giphy",
        "flag",
        "ban",
        "unban",
        "mute",
        "unmute"
      ]
    }
  },
  "created_at": "2019-04-24T09:49:48.630913Z"
}
                    

user.updated


{
  "type": "user.updated",
  "user": {
    "id": "thierry-7b690297-98fa-42dd-b999-a75dd4c7c993",
    "role": "user",
    "online": false,
    "awesome": true
  },
  "created_at": "2019-04-24T12:54:58.956621Z",
  "members": []
}
                    

Debugging with Ngrok

Ngrok is a tool allows you to expose a port on your local machine to the internet, allowing you to receive and monitor incoming requests from external sources such as webhooks. This makes Ngrok a perfect fit for debugging webhook payloads that come from Stream Chat.

Step 1. Installation

Ngrok can be installed on macOS using Homebrew with the following command:


$ brew cask install ngrok
                    

Alternatively, you can download Ngrok from their website at https://ngrok.com/. Once downloaded and unzipped, place the Ngrok executable in your applications directory (macOS).

Installation instructions for environments other than macOS can be found on the Ngrok website.

Create a symlink using the following commands:


# cd into your local bin directory
$ cd /usr/local/bin

# create symlink
$ ln -s /Applications/ngrok ngrok
                    
The above symlink will allow you to run the ngrok command from any directory while in the terminal. Without the symlink, you would need to either cd into the Applications directory (or wherever you installed the executable) or reference ngrok with its full path every time.

Once installed, you can safely move onto the next step.

Step 2. Ngrok Configuration

Now that Ngrok is properly installed, we'll need to spin it up on port 80 using the following command:


$ ngrok http 80
                    

Once you execute the command listed above, Ngrok will spin up a "forwarding URL" that you can use to specify in the Stream Dashboard. The output of the above command will look like this:

Now that your forwarding URL is available and online, copy the HTTPS forwarding URL as we will need it in the next step.

Step 3. Dashboard Configuration

With your forwarding URL copied, login to your Stream Chat dashboard and scroll down to the webhook section. Activate the webhook and then paste your HTTPS forwarding URL in the webhook URL input . Click save to persist your settings. Your dashboard will now look similar to the image below.

Step 4. Receiving Events

One of the many helpful tools that Ngrok provides is a web interface for inspecting incoming payloads. If you reference your terminal, you'll notice that there is a link for the "Web Interface" that runs on a local port – generally port 4040 if available. Navigate to http://localhost:4040 and you will see a dashboard for Ngrok.

Next, fire off an event such as a message from your chat interface to receive and inspect the payload. Once sent, the webhook will forward the payload to your Ngrok server for inspection via a POST request.

Ngrok will intermittently return a 502 Bad Gateway response. Please do not be alarmed by this as debugging with Ngrok is only for debugging purposes. In a production or staging environment, your server should return a 200 status code.

Below is an example of the payload for a message:


{
    "cid": "messaging:MYH-HwwO",
    "type": "message.new",
    "message": {
        "id": "2ecb4159e50b38cfb96e8ad2c4febd69-1ea9aa03-3edf-4491-8619-b34cabd4bcfc",
        "text": "Hello!",
        "html": "

Hello!

\n", "type": "regular", "user": { "id": "2ecb4159e50b38cfb96e8ad2c4febd69", "role": "admin", "created_at": "2019-09-16T13:35:08.977932Z", "updated_at": "2019-12-04T23:28:27.744384Z", "last_active": "2019-12-04T23:28:27.743804Z", "online": true, "image": "https://ui-avatars.com/api/?name=nick_parsons&size=192&background=000000&color=6E7FFE&length=1", "name": "nick_parsons", "username": "nick_parsons" }, "attachments": [], "latest_reactions": [], "own_reactions": [], "reaction_counts": null, "reaction_scores": {}, "reply_count": 0, "created_at": "2019-12-04T23:28:35.561344Z", "updated_at": "2019-12-04T23:28:35.561344Z", "mentioned_users": [] }, "user": { "id": "2ecb4159e50b38cfb96e8ad2c4febd69", "role": "admin", "created_at": "2019-09-16T13:35:08.977932Z", "updated_at": "2019-12-04T23:28:27.744384Z", "last_active": "2019-12-04T23:28:27.743804Z", "online": true, "channel_last_read_at": "1970-01-01T00:00:00Z", "total_unread_count": 0, "unread_channels": 0, "unread_count": 0, "image": "https://ui-avatars.com/api/?name=nick_parsons&size=192&background=000000&color=6E7FFE&length=1", "name": "nick_parsons", "username": "nick_parsons", "channel_unread_count": 0 }, "watcher_count": 1, "created_at": "2019-12-04T23:28:35.566646131Z", "channel_type": "messaging", "channel_id": "MYH-HwwO" }

Introduction

Stream Chat supports push for both Android and iOS. In this section you will find all information on how to configure push for APN and Firebase.

Only new messages are pushed to mobile devices, all other chat events are only send to WebSocket clients and webhook endpoints if configured.

Push Delivery Logic

Push message delivery follows the following logic:

  • Only channel members can receive push messages
  • Members that are currently online do not receive push messages
  • Messages added within a thread are only sent to users that are part of that thread (they posted at least one message or were mentioned)
  • Messages from muted users are not sent
  • Messages are sent to all registered devices for a user (up to 25)

iOS & APN

Push Notifications for iOS

Using the APNs, your users apps can receive push notifications directly on their client app for new messages when offline. Stream supports both Certificate-based provider connection trust(.p12 certificate), as well as Token-based provider connection trust(JWT).

Setup APN Push Using Token Authentication

Token based authentication is the preferred way to setup push notifications. This method is easy to setup and provides strong security.

Step 1. Retrieve your Team ID

Sign in to your Apple Developer Account and then navigate to Membership. Copy your Team ID and store it somewhere safe.

Step 2. Retrieve your Bundle ID

  • From App Store Connect, navigate to My Apps
  • Select the app you are using Stream Chat with
  • Make sure the App Store tab is selected and navigate to App Information on the left bar
  • In the Bundle ID dropdown, make sure the proper bundle id is selected. Copy the Bundle ID.

Step 3. Generate a Token

  • From your Apple Developer Account overview, navigate to Certificates, Identifiers & Providers
  • Make sure iOS, tvOS, watchOS is selected on the navigation pane on the left, and go to Keys > All
  • Click on the + button to Add a new key
  • In the Name field input a name for your key. In the Key Services section, select Apple Push Notifications service (APNs) and then click on Continue
  • Review the information from the previous step and click on Confirm
  • Copy your Key ID and store it somewhere safe
  • Save the key on your hard drive
You can only download your key at the time of generation, so please store this in a secure location.

Step 4. Upload the Key Credentials to Stream Chat

Upload the TeamIDKeyIDKey and BundleID from the previous steps.


await client.updateAppSettings({
    apn_config: {
        auth_key: fs.readFileSync(
            './auth-key.p8',
            'utf-8',
        ),
        auth_type: 'token',
        key_id: 'key_id',
        bundle_id: 'com.apple.test',
        team_id: 'team_id',
        notification_template: `{"aps" :{"alert":{"title":"{{ sender.name }}","subtitle":"New direct message from {{ sender.name }}","body":"{{ message.text }}"},"badge":"{{ unread_count }}","category":"NEW_MESSAGE"}}`
    },
});
                    
The upload task above can be completed using the CLI which supports Stream Chat. To install the CLI, simply run npm install -g getstream-cli OR yarn global add getstream-cli. More information on initializing the CLI can be found here.

If your wish to use the APNs development endpoint instead of the production one, you must specify this when uploading the Key Credentials via the development parameter as shown below:


await client.updateAppSettings({
    apn_config: {
        auth_key: fs.readFileSync(
            './auth-key.p8',
            'utf-8',
        ),
        key_id: 'key_id',
        auth_type: 'token',
        development: true,
        bundle_id: 'com.apple.test',
        team_id: 'team_id',
        notification_template: `{"aps" :{"alert":{"title":"{{ sender.name }}","subtitle":"New direct message from {{ sender.name }}","body":"{{ message.text }}"},"badge":{{ unread_count }},"category":"NEW_MESSAGE"}}`
    },
});
                    

Setup APN Push Using Certificate Authentication

If token based authentication is not an option, you can setup APN with Certificate Authentication. You will need to generate a valid .p12 certificate for your application and upload it to Stream Chat.

Step 1. Create a Certificate Signing Request (CSR)

  • On your Mac, open Keychain Access
  • Go to Keychain Access > Certificate Assistant > Request a Certificate from a Certificate Authority
  • Fill out the information in the Certificate Information window as specified below and click Continue.

In the User Email Address field, enter the email address to identify with this certificate. In the Common Name field, enter your name. In the Request group, click the "Save to disk" option.

Finally, save the file on your hard drive in secure area.

Step 2. Create a Push Notification SSL Certificate

  • Make sure iOS, tvOS, watchOS is selected on the navigation pane on the left, and go to Certificates > All
  • Click on the + button to Add a new certificate
  • In the Development section, select Apple Push Notification service SSL (Sandbox) and then click on Continue
  • Select your app in the dropdown list and then click on Continue
  • You will see instructions on how to generate a .certSigningRequest file. This was already covered in the previous section. Click on Continue
  • Click on Choose File and then navigate to where you have saved the .certSigningRequest file from the previous section, then click on Continue
  • Click on Download to save your certificate to your hard drive

Step 3. Export the Certificate in .p12 Format

  • On your mac, navigate to where you have saved the .cer file from the previous section and double click on the file. This will add it to your macOS Keychain.
  • Go to Keychain Access
  • At the top left, select Keychains > Login
  • Then, at the bottom left, select Category > Certificates
  • Select the certificate you've created in the previous step. It should look like Apple Development IOS Push Services: YOUR_APP_NAME and expand it to see the private key(it should be named after the Name you provided when creating the Certificate Signing Request – the case of this example: John Smith)
  • Right-click the private key and click on Export. In the File format section select Personal Information Exchange (.p12) and save the file on your hard drive

Step 4. Upload the Certificate to Stream Chat


await client.updateAppSettings({
    apn_config: {
        p12_cert: fs.readFileSync(
            './certificate.p12',
        ),
        auth_type: 'certificate',
        notification_template: `{"aps":{"alert":{"title":"{{ sender.name }}","subtitle":"New direct message from {{ sender.name }}","body":"{{ message.text }}"},"badge":{{ unread_count }},"category":"NEW_MESSAGE"}}`
    },
});
                    
If your wish to use the APNs development endpoint instead of the production endpoint, this information will be automatically taken from your certificate.

Android & Firebase

Push Notifications for Android and Web

If you're looking to speed up your Android app development, you can use our Android Chat SDK to get started. It's feature rich and will speed up your development!

Using Firebase, your users apps can receive push notifications directly to their client app for new messages when offline. In order to push notifications to Android devices, you need to have an application on Firebase and configure your Stream account using the Firebase server key.

Retrieving the Server Key from Firebase

Step 1

From the Firebase Console, select the project your app belongs to

Step 2

Click on the gear icon next to Project Overview and navigate to Project settings

Step 3

Navigate to the Cloud Messaging tab

Step 4

Under Project Credentials, locate the Server key and copy it

Step 5

Upload the Server Key in your chat dashboard

Step 6

Save your push notification settings changes

OR -

Upload the Server Key via API call


await client.updateAppSettings({
    firebase_config: {
        server_key: 'server_key',
        notification_template: `{"message":{"notification":{"title":"New messages","body":"You have {{ unread_count }} new message(s) from {{ sender.name }}"},"android":{"ttl":"86400s","notification":{"click_action":"OPEN_ACTIVITY_1"}}}}`,
        data_template: `{"sender":"{{ sender.id }}","channel":{"type": "{{ channel.type }}","id":"{{ channel.id }}"},"message":"{{ message.id }}"}`
    },
});
                    

Devices

Once your app has push enabled, you can use the APIs to register user devices such as iPhones and Android phones.

Each chat user has a limit of 25 unique devices. Once this limit is reached, the oldest device will be removed and replaced by the new device.

Device Parameters

Name Type Description Default Optional
user_id string The user ID for this device -
id string The device ID. -
provider string The push provider for this device. Either APN or firebase. -

Register a Device


await client.addDevice(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    'apn', 
    '42'
)
                    

client.add_device(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    'apn', 
    '42'
)
                    

client.add_device(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    'apn', 
    '42'
)
                    

client.addDevice(deviceToken, new CompletableCallback() { ... });
                    

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Client.shared.addDevice(deviceToken: deviceToken).subscribe().disposed(by: disposeBag)
}
                    

stream chat:push:device:add \
    --device_id '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207'
    --user_id '42'
    --provider 'apn'
                    

Unregister a Device


await client.removeDevice(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    '42'
)
                    

client.delete_device(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    '42'
)
                    

client.delete_device(
    '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207', 
    '42'
)
                    

client.removeDevice(deviceToken, new CompletableCallback() { ... });
                    

Client.shared.removeDevice(deviceId: device.id).subscribe().disposed(by: disposeBag)
                    

stream chat:push:device:delete \
    --device_id '2ffca4ad6599adc9b5202d15a5286d33c19547d472cd09de44219cda5ac30207'
    --user_id '42'
                    

Push Templates

For both Firebase and APN, the payload that is being sent is rendered using the handlebars templating language, to ensure full configurability for your app.

Stream provides the following variables in the template rendering context:

Context Variables

Name

Type

Description

channel

object

Channel object. You can access the channel name and any other custom field you have defined for this channel

sender

object

Sender object. You can access the user name, id or any other custom field you have defined for the user

receiver

object

Receiver object. You can access the user name, id or any other custom field you have defined for the user

message

object

Message object. You can access the text of the message (or a preview of it if the message is too large) or any other custom field you have defined for the message

members

array

Channel members. You can access the user name, id and any other custom field of each member

otherMembers

array

Like members but the user who will be receiving the notification is excluded

unread_count

integer

Number of unread messages

unread_channels

integer

Number of unread channels for this user

Defaults

When editing APN/Firebase settings, if you leave the notification_template or data_template field empty, default templates will be used.

APN default:


{
    "aps" : {
        "alert" : {
            "title" : "{{ sender.name }} @ {{ channel.name }}",
            "body" : "{{ message.text }}"
        },
        "badge": {{ unread_count }},
        "category" : "NEW_MESSAGE"
    }
}
                    

Firebase default notification template:


{
    "title": "{{ sender.name }} @ {{ channel.name }}",
    "body": "{{ message.text }}",
    "click_action": "OPEN_ACTIVITY_1",
    "sound": "default"
}
                    

Firebase default data template:


{
    "sender": "{{ sender.id }}",
    "channel": {
        "type": "{{ channel.type }}",
        "id": "{{ channel.id }}"
    },
    "message": "{{ message.id }}"
}
                    

Limitations

There are some limitations that Stream imposes on the push notification handlebars template to make sure no malformed payloads are being sent to push providers.

1: Custom Arrays Can't Be Indexed

For example, given the context:


{
    "sender":{
        "name": "Bob",
        "some_array": ["foo","bar"]
    }
}
                    

And the template:


"title": {{sender.some_array.[0]}}
                    

The rendered payload will be:


"title": ""
                    

2: Interpolating Whole Lists and Objects Isn't Allowed

For example, given the context:


{
    "sender":{
        "name": "bob",
        "some_array": ["foo","bar"],
        "address": {
            "street": "willow str"
        }
    }
}
                    

And the template:


"title": "{{ sender.some_array }} {{ sender.address }}"
                    

The rendered payload will be:


"title": "[] {}"
                    

3: Unquoted fields that aren't in the context will be rendered as empty strings

For example, given the context:


{
    "sender":{
        "name": "bob"
    }
}
                    

And the template:


"title": {{ sender.missing_field }}
                    

The rendered payload will be:


"title": ""
                    

Advanced Use Cases

For advanced use cases (e.g. A list of channel members in the notification title, conditional rendering, etc), Stream provides some handlebars helper functions.

Helper Functions

name

type

description

implodemembers

function

takes the list of channel members and implodes it into a single string, using a custom limit, separator and suffix.

json

function

renders passed parameter as JSON (e.g {"channel":{{{ json channel }}}})

each

function

For loop. Use this to access the current variable, @index for the current index and @first and @last as convenience booleans to determine if the iteration is at its first/last element

if

function

If function. Tests trueness of given parameter. Supports else statement. (e.g {{#if sender.name}}{{ sender.name }}{{/if}})

unless

function

Unless function. Tests falseness of given parameter. Supports else statement. (e.g {{#unless sender.name}}Missing name{{/unless}})

equal

function

Equality check function. Tests equality of the given 2 parameters. Supports else statement. (e.g {{#equal channel.type "messaging" }}This is the messaging channel{{else}}This is another channel{{/equal}} )

unequal

function

Inequality check function. Tests inequality of the given 2 parameters. Supports else statement. (e.g {{#unequal channel.type "messaging" }}This is another channel{{else}}This is the messaging channel{{/unequal}} )

ifLt

function

If less than. Supports else statement.

ifLte

function

If less than or equal. Supports else statement.

ifGt

function

If greater than. Supports else statement.

ifGte

function

If greater than or equal. Supports else statement.

remainder

function

Calculates the difference between the length of an array and an integer (e.g {{remainder otherMembers 2}}

Most of the functions above are straight forward, except for implodeMembers, which will be detailed further.

The full function signature is: {{implodeMembers otherMembers|members [limit=] [separator=] [nameField=] [suffixFmt=]}}

Function Parameters

name

type

description

default

otherMembers | members

array

Which member array to implode

limit

integer

How many member names to show before adding the suffix

3

nameField

string

Field name from which field to retrieve the member's name. Note: does not support nesting

name

separator

string

Separator to use for channel members

,

suffixFmt

string

Format string to use for the suffix. Note: only %d is allowed for formatting

and %d other(s)

Examples

Let's put these helpers to use in a few examples:

Example 1

Rendering channel members in the notification title. Each member's name is stored in the fullName field.

What we want to achieve:


{
    "aps": {
        "alert": {
            "title": "Bob Jones, Jessica Wright, Tom Hadle and 4 other(s)",
            "body": "Bob Jones: Hello there fellow channel members"
        },
        "badge": 0
    }
}
                    

How we will achieve it: using implodeMembers with a custom name field (leaving others empty so that defaults will be used):


{
    "aps": {
        "alert": {
            "title": "{{implodeMembers otherMembers nameField="fullName"}}",
            "body": "{{ sender.fullName }}: {{ message.text }}"
        },
        "badge": {{ unread_count }}
    }
}
                    

Example 2

Rendering channel members in the notification title. Each member's name is stored in the nested details.name field.

What we want to achieve:


{
    "aps": {
        "alert": {
            "title": "Bob Jones, Jessica Wright, Tom Hadle and 4 other(s)",
            "body": "Bob Jones: Hello there fellow channel members"
        },
        "badge": 0
    }
}
                    

How we will achieve it: since implodeMembers doesn't support nested fields, we need to use a bunch of helpers such as each, ifLte. Note how the use of ~ will trim the whitespaces so that the title in rendered in a single row:


{
    "aps": {
        "alert": {
            "title": "
            {{~#each otherMembers}}
                {{#ifLte @index 2}}
                    {{~this.details.name}}{{#ifLt @index 2 }}, {{/ifLt~}}
                {{~else if @last~}}
                    {{{ " " }}} and {{remainder otherMembers 3}} other(s)
                {{~/ifLte~}}
            {{/each~}}",
            "body": "{{ sender.details.name }}: {{ message.text }}"
        },
        "badge": {{ unread_count }}
    }
}
                    

Push Test

Once you're all setup with push notifications, you can use the CLI to test how the these notifications will look for your devices.

In preparation of this make sure that:

  • Your app has push notifications configured for at least one provider(APNs or Firebase)

  • You have a user that has at least one device associated

The base command for testing push notifications is:


stream chat:push:test --user_id 'user_123'
                    

This will do several things for you:

  1. Pick a random message from a channel that this user is part of

  2. Use the notification templates configured for your push providers to render the payload using this message

  3. Send this payload to all of the user's devices

This particular use case is ideal for testing a newly configured app, to make sure the push notifications are exactly as wanted.

In some other cases, your app is already configured with push notifications and is running smoothly, but you want to try out a new notification template. For example, let's say you want to test a new APN notification template:


stream chat:push:test --user_id 'user_123' --apn_notification_template '{"aps":{"alert":{"title":"{{ sender.name }} @ {{ channel.name }}","body":"testing out new stuff:{{ message.text }}"},"category":"NEW_MESSAGE_2"}}'
                    

As you can see, this one time only, the new template will be used for the push notification instead of the configured one:

Here's a full list of parameters that you can use with the test command:

Push Test Parameters

Name Type Description Default Optional
user_id string The user ID -
message_id string ID of the message that should be used instead of a random one. If the message doesn't exist, an error will occur -
apn_notification_template string Notification template to be used instead of the configured APN one. This is one time only. -
firebase_notification_template string Notification template to be used instead of the configured APN one. This is one time only. -
firebase_data_template string Data template to be used instead of the configured Firebase one. This is one time only. -

Initial Setup

To try out push notifications on an actual device, you can use React Native to quickly build an app that just receives notifications.

This tutorial is not supported in Expo applications. We need access to the native code to get the device tokens required to send push notifications for Stream Chat. If you are hoping to integrate this into an existing Expo application, you will need to eject.

Shortcut

If you don't want to jump through all the hoops described in the following sections, we have a mostly preconfigured repository with everything described in this doc on GitHub. Just follow the instructions in the README to get started.

Before getting down to business with React Native, a few things need to be setup first:

Step 1 - Create a Development App

Make sure you’ve created a development app within the dashboard.

Step 2 - Disable Auth

To make things easier, we are going to disable auth so that we can easily test push messages. Please note that this only suitable for test/dev environments.

From your app's dashboard, navigate to the Chat tab and then scroll down to Chat Events. Make sure the Disable Auth Checks toggle is On and click on Save.

Step 3 - XCode Setup

Install XCode and the Command Line Tools. More info on that here.

Step 4 - Create the Test Project

Install dependencies and initialize your test project (for this tutorial we'll use the name chat-push-example)

If you are adding this to an existing React Native project created with Expo or CRNA, you will need to eject. Please note that this ejecting is; however, it is required to access native iOS and Android capabilities.

yarn global add react-native-cli
                    

react-native init chat-push-example
                    

cd chat-push-example
yarn add stream-chat react-native-push-notifications
react-native link react-native-push-notifications
                    

iOS Setup

For iOS, we will be using Apple’s APN service to power the push functionality, rather than Expo or Firebase Cloud Messaging.

You will need to link the PushNotificationIOS library, which is exported from React Native, to your project:

Step 1 - Open the Add Files Dialog

Open Xcode and in the sidebar on the left, make sure you’re in the Project Navigator tab (the folder icon). Right click on the Libraries directory within your project name and select Add files to YOUR_PROJECT_NAME.

Step 2 - Select the RCTPPushNotification Project

A file dialog will pop-up. Navigate to the root of your project, then to node_modules/react-native/Libraries/PushNotificationsIOS/RCTPushNotification.xcodeproj. Click Add.

Step 3 - Link the Binaries

Click the root of your application's Xcode project in the navigator sidebar and click Build Phases in the tabs at the top of the middle pane. On the next screen, find the dropdown labeled Link Binaries with Libraries, expand it, and click the plus icon in the bottom left.

Type RCTPushNotification in the search box. You should see a file named libRCTPushNotification.a in the list. Select it and click Add.

Step 4 - Setup the Imports

Navigate to AppDelegate.m in your project.

Add the following import just below #import "AppDelegate.h".


#import "AppDelegate.h"

#import  // <-- THIS
                    

Step 5 - Integrate

Add the following snippet to the bottom of the file, just before @end.


// Required to register for notifications
- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings
 {
  [RCTPushNotificationManager didRegisterUserNotificationSettings:notificationSettings];
 }
 // Required for the register event.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
 {
  [RCTPushNotificationManager didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
 }
 // Required for the notification event. You must call the completion handler after handling the remote notification.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
                                                        fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
 {
   [RCTPushNotificationManager didReceiveRemoteNotification:userInfo fetchCompletionHandler:completionHandler];
 }
 // Required for the registrationError event.
- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
 {
  [RCTPushNotificationManager didFailToRegisterForRemoteNotificationsWithError:error];
 }
 // Required for the localNotification event.
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
 {
  [RCTPushNotificationManager didReceiveLocalNotification:notification];
 }
                    

Running On iOS Devices

Unfortunately, push notifications do not work in the iOS simulator, so you’re going to need a physical iOS device to be able to see the fruits of your labor.

If you want to run your React Native application on a device, you’re going to have to edit the XCode project to include code signing and push capabilities:

Step 1:

In XCode, from the navigation bar on the left make sure your project is selected. Navigate to the General tab.

Step 2

Expand Signing and make sure you are logged in with your Apple Developer Account. Once logged in, select your development team and be sure to check Automatically Manage Signing.

Step 3

Expand Identity and make sure your Bundle Identifier matches the one you used to configure push notifications with Stream. If you haven’t done this yet, please refer to the docs here and then return to this tutorial.

Step 4

Navigate to the Capabilities tab and make sure Push Notifications are enabled

Step 5

You’re all set! Plug in your iOS device, select it from the run menu in XCode and press Run.

Android React Native Setup

For Android, we’ll be using Firebase Cloud Messaging to power push notifications:

Step 1 - Get your google-services.json

  • Go to the Firebase Console, create a new application OR select an existing project.

  • Go to Project Settings and under the General tab.

  • Click on Your Apps, add an Android application, and download your google-services.json file – you need to put this in the root of your projects android directory.

Step 2 - Configure google-services

Put google-services.json in the root of your project's android directory (e.g. ./android.

Make sure google-services.json file is included in your Android project’s dependencies by navigating to your project level build.gradle file (./android/build.gradle) and adding the following line:


buildscript {
	// ...
	dependencies {
		// ...
		classpath 'com.google.gms:google-services:+'
		// ...	
	}
	// ...
}
                    

Step 3 - Setup dependencies

In the same directory, find the settings.gradle file and copy the following content into the file if it isn’t already there


include ':react-native-push-notification'
project(':react-native-push-notification').projectDir = file('../node_modules/react-native-push-notification/android')
                    
When you previously ran react-native link, it should have added the necessary files; however, it’s best to always double check. Linking can be temperamental at times.

Navigate to ./android/app/src and check you have the res folder. If not, create it and inside create another folder values. Then create a new file named colours.xml whose content is the following



   #FFF

                    

Linking may have also taken care of this step, but once again, navigate to  MainApplication.java  in  android/app/src/main and check that it has these two parts, as highlighted with comments:


import com.dieam.reactnativepushnotification.ReactNativePushNotificationPackage;  // <--- THIS

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
      @Override
      protected boolean getUseDeveloperSupport() {
        return BuildConfig.DEBUG;
      }
                    

Go to android/app/src/main/AndroidManifest.xml and copy the following (the comments below will help guide you for your particular setup)



  
  
   

   
   
   



  
    
        
        
    
  

  
  
    
      
    
  
  

  
    
      
    
  

  
    
        
    

                    

Unlike iOS, the Android emulator does support push notifications - just make sure you use an emulator with Google Play Services installed (shown by the Play Store Icon in the AVD Manager) 

Integration with Stream

Thanks to react-native-push-notifications, we are provided a platform agnostic experience while still using native push on each platform.

All that's left: integrating the test app with the Stream Chat client. Open up App.js in the root of your project and check that componentDidMount exists. If the file does not exist add an async componentDidMount method to the class:


//...
import { PushNotificationIOS } from 'react-native';
import PushNotification from 'react-native-push-notification';
import { StreamChat } from 'stream-chat';
import { API_KEY, USER_TOKEN, USER_ID } from 'react-native-dotenv';

export default class App extends Component {

    async componentDidMount() {
        try {
            const client = new StreamChat(API_KEY, null);
    
            await client.setUser({ id: USER_ID }, USER_TOKEN);
    
            PushNotification.configure({
                onRegister(token) {
                    await client.addDevice(token.token, token.os === 'ios' ? 'apn' : 'firebase');
                },
                onNotification(notification) {
                    notification.finish(PushNotificationIOS.FetchResult.NoData);
                },
                senderID: "YOUR_SENDER_ID",// (Android Only) Grab this from your Firebase Dashboard where you got google-services.json
                requestPermissions: true
            });
        } catch(error) {
            console.log(error)
        }
    }
    
    //...
}
                    

For the sake of this tutorial, we created a .env file to store these values and grab them using react-native-dotenv as shown above – a good idea for your API_KEY in any case, but your USER_ID and USER_TOKEN will likely come from elsewhere in a real-world setting, depending on your use-case.

All that's left to do is to use the stream-cli to send out a test push notification. You can find out how that works in this section of the docs. Just make sure you use the same user_id in both the cli and this test app.

How To

To build your own chat bot you can use the following 4 features of the Stream Chat API:

  1. Webhooks
  2. Attachments, Fields and Actions
  3. Custom Slash Commands
  4. Custom Attachments

Step 1 - Webhook Setup

As an example let's say that you want to build a chatbot that handles customer care for a bank. You'll typically want to gather some data automatically before routing the request to a human. To achieve that you would start by setting up a webhook (webhook docs). The webhook will be called whenever there is a new message on the channel.

Step 2 - AI

When handling the webhook you'll want to run some AI to determine what the user is talking about. A good starting point is recognizing the most common requests. For instance, your AI could recognize a message like this: "I noticed a weird transaction on my account." Next, the AI can reply by asking the user for more details leveraging the attachment, field, and action system.

Step 3 - Attachment, Fields and Actions

Let's have the AI reply with a message asking for more information.


const message = {
    text: 'Which account was affected by this issue?',
    attachments: [
        {
            type: 'form',
            title: 'Select your account',
            actions: [
                {
                    name: 'account',
                    text: 'Checking',
                    style: 'primary',
                    type: 'button',
                    value: 'checking',
                },
                {
                    name: 'account',
                    text: 'Saving',
                    style: 'default',
                    type: 'button',
                    value: 'saving',
                },
                {
                    name: 'account',
                    text: 'Cancel',
                    style: 'default',
                    type: 'button',
                    value: 'cancel',
                },
            ],
        },
    ],
};
const response = await channel.sendMessage(message);
                    

When the user submits their choice, the webhook endpoint will be called again. Now that you have some more information gathered it's time to connect the user to a real support agent. You can do this by adding a member to the conversation and your support agent will be notified in real time. To improve their productivity you'll want to leverage slash commands. Slash commands make it easy to automate common tasks. The next step will show you how to create your own slash command for managing tickets.

Step 4 - Slash Commands

First, you'll want to setup a custom slash command for the "ticket" keyword. At the moment this isn't fully documented, so contact support if you need help. With the slash command setup your account managers will be able to automate common tasks such as: /ticket suspicious transaction with id 1234


const message = {
    text: '/ticket suspicious transaction with id 1234'
};
const response = await channel.sendMessage(message);
                    

Step 5 - Custom Attachments

Custom attachments can also be helpful when building chat bots. For example, you could create a custom attachment for allowing users to select a date. The React Chat tutorial shows an example of how to create a custom attachment.

Operators

The Stream Chat API allows you to specify filters and ordering for several endpoints. You can query channels, users and messages. The query syntax is similar to that of Mongoose.

We do not run MongoDB on the backend. Only a subset of the MongoDB operations are supported.

Please have a look below at the complete list of supported query operations:

Name

Description

Example

$eq

Matches values that are equal to a specified value.

{ "key": { "$eq": "value" } }

or the simplest form

{ "key": "value" }

$gt

Matches values that are greater than a specified value.

{ "key": { "$gt": 4 } }

$gte

Matches values that are greater than or equal to a specified value.

{ "key": { "$gte": 4 } }

$lt

Matches values that are less than a specified value.

{ "key": { "$lt": 4 } }

$lte

Matches values that are less than or equal to a specified value.

{ "key": { "$lte": 4 } }

$ne

Matches all values that are not equal to a specified value.

{ "key": { "$ne": "value" } }

$in

Matches any of the values specified in an array.

{ "key": { "$in": [ 1, 2, 4 ] } }

$nin

Matches none of the values specified in an array.

{ "key": { "$in": [ 3, 5, 7 ] } }

$and

Matches all the values specified in an array.

{ "$and": [ { "key": { "$in": [ 1, 2, 4 ] } }, { "some_other_key": 10 } ] }

$or

Matches at least one of the values specified in an array.

{ "$or": [ { "key": { "$in": [ 1, 2, 4 ] } }, { "key2": 10 } ] }

$nor

Matches none of the values specified in an array.

{ "$nor": [ { "key": { "$in": [ 1, 2, 4 ] } }, { "key2": 10 } ] }

Importing Data

If you've just started using Stream, you might want to import data from your infrastructure or provider. Instead of using the APIs and creating your own import scripts, you can make use of our import feature.

The Process

The steps for importing data into your app are as follows:

  1. Generate the import file for your data (full file reference below)
  2. Reach out to send us the import file

  3. The file will be validated according to the rules described in this doc
  4. If validation passes, a member of our team will approve and run the import
  5. Once the import is completed (usually a few minutes), you will get a confirmation email
Before uploading the import file make sure that every feed group in your import file is configured in your app.
Importing data on a live app may cause high response times for requests during the import process.

Import File

The import file must be structured as a JSON array of objects. Each object is an item to add to your application (eg. a user, a message, etc.).

Here's an example of an import file:


[
	{
		"type": "channel",
		"item": {
			"id": "6e693c74-262d-4d3d-8846-686364c571c8",
			"type": "livestream",
			"created_by": "aad24491-c286-4169-bbfc-9280de419cb6",
			"name": "Rock'n Roll Circus"
		}
	},
	{
		"type": "message",
		"item": {
			"id": "977e691a-c091-4e3b-8f70-ba8944a3e500",
			"channel_type": "livestream",
			"channel_id": "6e693c74-262d-4d3d-8846-686364c571c8",
			"user": "aad24491-c286-4169-bbfc-9280de419cb6",
			"text": "Such a great song, check out my solo at 2:25",
			"type": "regular",
			"created_at": "2017-02-01T02:00:00Z",
			"attachments": [
				{
					"type": "video",
					"url": "https://www.youtube.com/watch?v=flgUbBtjl9c"
				}
			]
		}
	},
	{
		"type": "reaction",
		"item": {
			"message_id": "977e691a-c091-4e3b-8f70-ba8944a3e500",
			"type": "love",
			"user_id": "aad24491-c286-4169-bbfc-9280de419cb6",
			"created_at": "2019-03-02T15:00:00Z"
		}
	}
]
                    

Import Entries Format

Name

Type

Description

type

string

the type of data for this entry. Allowed values are: user, channel, message, reaction, member

item

object

the data for this entry, see below for the format of each type

Item Types

An import file can contain entries with any of these types.

Note that you can add custom fields to users, channels, members, messages, attachments and reactions. The limit is 5KB of custom field data per object.

User Type

The user type fields are shown below:

Name Type Description Default Optional
id string the unique id for the user
role string the role for the user user
created_at string the creation time of the user in RFC3339
* string/list/object add as many custom fields as needed. up to 5KB


{
  "type": "user",
  "item": {
    "id": "aad24491-c286-4169-bbfc-9280de419cb6",
    "name": "Jesse",
    "profile_image": "http://getstream.com",
    "created_at": "2017-01-01T01:00:00Z",
    "description": "Taj Mahal guitar player at some point"
  }
}
                    

Channel Type

The channel type fields are shown below:

Name Type Description Default Optional
id string the unique id for the channel
type string the type of the channel. ie livestream, messaging etc.
created_by string the id of the user that created the message
frozen boolean you can't add messages to a frozen channel false
* string/list/object add as many custom fields as needed


{
  "type": "channel",
  "item": {
    "id": "6e693c74-262d-4d3d-8846-686364c571c8",
    "type": "livestream",
    "created_by": "aad24491-c286-4169-bbfc-9280de419cb6",
    "name": "Rock'n Roll Circus"
  }
}
                    

Member Type

Channel members store the mapping between users and channels. The fields are shown below:

Name Type Description Default Optional
channel_type string the type of channel for this member
channel_id string the id of the channel
user_id string the user id
is_moderator boolean true if the user is a channel moderator false
created_at string the membership creation time, in RFC3339 current time


{
  "type": "member",
  "item": {
    "channel_type": "livestream",
    "channel_id": "6e693c74-262d-4d3d-8846-686364c571c8",
    "user_id": "aad24491-c286-4169-bbfc-9280de419cb6",
    "is_moderator": true,
    "created_at": "2017-02-01T02:00:00Z"
  }
}
                    

Message Type

message creation time in RFC3339 format

last time the message was updated

message deletion time in RFC3339 format

reply messages are only shown inside the message thread unless show_in_channel is set to true, in that case the message is shown in the channel as well

Add as many custom fields as needed

Name Type Description Default Optional
id string the id of the message A random UUIDv4
channel_type string the type of channel for this message
channel_id string the id of the channel for this message
type string the type of the message, regular, deleted or system regular
user string the id of the user that posted the message
attachments list of attachments list of message attachments, see the attachment section below []
parent_id string the id of the parent message (if the message is a reply) null
created_at string
updated_at string created_at
deleted_at string null
show_in_channel bool false
* string/list/object


{
  "type": "message",
  "item": {
    "id": "977e691a-c091-4e3b-8f70-ba8944a3e500",
    "channel_type": "livestream",
    "channel_id": "6e693c74-262d-4d3d-8846-686364c571c8",
    "user":"aad24491-c286-4169-bbfc-9280de419cb6",
    "text": "Such a great song, check out my solo at 2:25",
    "type": "regular",
    "created_at": "2017-02-01T02:00:00Z",
    "attachments": [
      {
        "type": "video",
        "url": "https://www.youtube.com/watch?v=flgUbBtjl9c"
      }
    ]
  }
}
                    

Message Attachments

The only required field of an attachment is type. All other fields are optional and you can add as many custom fields as you'd like. The attachments are a great way to extend Stream's functionality. If you want to have a custom product attachment, location attachment, checkout, etc, attachments are the way to go. The fields below are automatically picked up and shown by our component libraries.

the type of attachment. Eg. text, audio, video, image

Name Type Description Default Optional
type string
fallback string
color string
pretext string
author_name string
author_link string
author_icon string
title string
title_link string
text string
image_url string
thumb_url string
footer string
footer_icon string
asset_url string
* string/list/object Add as many custom fields as needed

Reaction Type

The reaction type has the following fields:

Name Type Description Default Optional
message_id string The id of the message
type string The type of reaction (up to you to define the types, it's a string)
user_id string The ID of the user
created_at string The creation time in RFC3339
* string/list/object Add as many custom fields as needed


{
  "type": "reaction",
  "item": {
    "message_id": "977e691a-c091-4e3b-8f70-ba8944a3e500",
    "type": "love",
    "user_id": "aad24491-c286-4169-bbfc-9280de419cb6",
    "created_at": "2019-03-02T15:00:00Z"
  }
}
                    

SendBird Migration

Channel Types

SendBird has 2 channel types. The open channel or the group channel. Stream Chat has 5 built-in channel types: livestream, messaging, commerce, gaming, and team. You can also create your own channel types. This allows you to configure the features and permissions to exactly fit your use case. Usually, you’ll want to use the “livestream” chat type if you’re using a SendBird open channel.

Channels

Instead of the getChannel and channel.enter, Stream uses 1 API call to both get and enter a channel.

The concept of UserMessage and FileMessage in SendBird is replaced using a Message with a list of attachments.

Thumbnails

Instead of specifying thumbnail sizes upfront you can request different image sizes at read time.

Private vs. Public Groups

This difference is handled by Stream’s permission system. You can allow or not allow non-members to edit the list of members for a channel.

Limitations

  • Stream currently doesn’t directly support translating messages. However, you can write a custom bot to support this.
  • You can query users

Layer Migration

Migrating from Layer is easy. This guide will help you complete the migration quickly:

Export

The first step is downloading an export of your Layer data. The Layer docs specify how to create an export. Next email support@getstream.io your data export to have it imported to Stream. This process typically takes 1 business day.

Frontend Components

Layer doesn’t provide frontend components. You will want to decide if you want to customize one of Stream’s frontend components, or work with the chat API from your own frontend. Implementing a fully featured Chat in React can be very time consuming.

Have a look at these 5 examples and see if you can customize them. Swapping the front end components is the fastest way to integrate with Stream.

Differences

  • Stream provides 5 built-in chat types for the most common use cases. The commerce chat type is the most similar to Layer, so you’ll likely want to use it as a starting point.
  • Layer has the concept of Distinct Conversations and Non Distinct conversations. With Stream you initialize a channel with a channel type and channel id. You simply need to make sure the ID is unique when you want to create a new conversation.
  • Stream's naming conventions are slightly different from Layer's:

STREAM

LAYER

Message Attachments (docs here)

MessageParts

Channels

Conversations

Members

Participants

  • Another difference is that Layer allows you to specify a Metadata field. Stream allows you to add custom data directly to users, channels, messages, attachments and events.

Go Live

After testing your awesome new chat solution discuss a go-live date with Stream’s support team. We’ll re-import your data backup.

Help

If you have more tips for migrating from Layer to Stream be sure to contact us. We’re refining this guide continuously.

Introduction

Every Application is rate limited at the API endpoint. These limits are set on a 1-minute and 15-minute time windows, API requests have specific rate limits. For example, reading a channel has a different limit than sending a message.

User and App Base Rate Limits

There are two kind of rate limits for Chat: app rate limits and user rate limits. App rate limits are calculated per endpoint for your entire application. When the limit is hit, all calls from the same app and endpoint will result in an error. In order to avoid individual users to use your entire quota, every single user is limited to at most 60 requests per minute (per API endpoint). When the limit is exceeded, only request from that user will be rejected.

What Happens When You Are Rate Limited

If an API call exceeds a rate limit during the target time window, subsequent requests for the same API function will be rejected. The payload returned for that API call will indicate that rate limiting has occurred.

If your application becomes rate-limited it doesn't block other API calls from working. For example, hitting a rate limit on follows will still allow you to read feeds. Each "resource" is rate-limited separately.

Rate limits are reset at the end of each time window.

How to Avoid Rejected API Requests

For Enterprise plans, Stream will review your architecture, and set higher rate limits for your production application. For other paid plans, you will need to review responses from Stream to watch for error conditions indicating that your API request was rate-limited and retry. We recommend implementing an exponential back-off retry mechanism.

Rate Limit Headers

Header

description

X-RateLimit-Limit

the total limit allowed for the resource requested (ie. 5000)

X-RateLimit-Remaining

the remaining limit (ie. 4999)

X-RateLimit-Reset

when the current limit will reset (unix timestamp)

Rate Limits by Endpoint

Please reference the following table for basic plan rate limits by endpoint (on a per minute and per 15 minute basis):

API Request

Calls Per Minute

Calls Per 15 Minutes

Connect

10,000

100,000

Get or Create Channel

10,000

100,000

Mark All Read

10,000

100,000

Mark Read

10,000

100,000

Query Channels

10,000

100,000

Send Event

10,000

100,000

Delete Message

1,000

10,000

Get Message

1,000

10,000

Delete Reaction

1,000

10,000

Get Reactions

1,000

10,000

Get Replies

1,000

10,000

Query Users

1,000

10,000

Run Message Action

1,000

10,000

Send Message

1,000

10,000

Send Reaction

1,000

10,000

Stop Watching Channel

1,000

10,000

Update Message

1,000

10,000

Upload File

1,000

10,000

Upload Image

1,000

10,000

Create Guest

1,000

10,000

Ban

300

3,000

Edit Users

300

3,000

Flag

300

3,000

Mute

300

3,000

Search

300

3,000

Unban

300

3,000

Unflag

300

3,000

Unmute

300

3,000

Update Channel

300

3,000

Update Users

300

3,000

Create Device

300

3,000

Hide Channel

300

3,000

Show Channel

300

3,000

Update Users (Partial)

300

3,000

Create Channel Type

60

600

Deactivate User

60

600

Delete Channel

60

600

Delete Channel Type

60

600

Delete Device

60

600

Delete File

60

600

Delete User

60

600

Export User

60

600

Get App

60

600

Get Channel Type

60

600

List Channel Types

60

600

List Devices

60

600

Truncate Channel

60

600

Update App

60

600

Update Channel Type

60

600

Check Push

60

600

Activate User

60

600

The rate limits above are for general applications. Rate limits can be adjusted on a per need basis, depending on your use-case and plan.

Export User Data

To export user data, Stream Chat exposes an exportUser method. This method can only be called server-side due to security concerns, so please keep this in mind when attempting to make the call.

Below is an example of how to execute the call to export user data:


const data = await client.exportUser('user_id');
                    

response = client.export_user("user_id");
                    

response = @client.export_user('user_id')
                    

$response = $this->client->exportUser($user["id"]);
                    


                    

var exportedUser = await this._endpoint.Export(user1.ID);
                    

The export will return all data about the user, including:

  • User ID

  • Messages

  • Reactions

  • Custom Data

Users with more than 10,000 messages will throw an error during the export process. The Stream Chat team is actively working on a workaround for this issue and it will be resolved soon.

Deactivate a User

To deactivate a user, Stream Chat exposes a deactivateUser method. This method can only be called server-side due to security concerns, so please keep this in mind when attempting to make the call.

Below is an example of how to execute the call to deactivateUser:


const deactivate = await client.deactivateUser('user_id');
                    

response = client.deactivate_user("user_id")
                    

response = @client.deactivate_user('user_id')
                    

$response = $this->client->deactivateUser($user["id"]);
                    


                    

result = await this._endpoint.Deactivate([user.ID]);
                    

The response will contain an object with the user ID that was deactivated. Further, the user will no longer be able to connect to Stream Chat as an error will be thrown.

To reinstate the user as active, use the reactivateUser method by passing the users ID as a parameter:


const reactivate = await client.reactivateUser('user_id');
                    

response = client.reactivate_user("user_id")
                    

response = @client.reactivate_user('user_id')
                    

$response = $this->client->reactivateUser($user["id"]);
                    


                    

result = await this._endpoint.Reactivate([user.ID]);
                    

Delete a User

To delete user data, Stream Chat exposes a deleteUser method. This method can only be called server-side due to security concerns, so please keep this in mind when attempting to make the call.

Below is an example of how to execute the call to deleteUser:


const remove = await client.deleteUser('user_id', {
    mark_messages_deleted: false,
});
                    

response = client.delete_user(random_user["id"])
                    

response = @client.delete_user('user_id)
                    

$response = $this->client->deleteUser($user["id"]);
                    


                    

result = await this._endpoint.Delete([user.ID]);
                    
The mark_messages_deleted parameter is optional. This parameter will delete all messages associated with the user. If you would like to keep message history, ensure that mark_messages_deleted is set to false. To remove all messages related to the user, set the value to true.

To perform a "hard delete" on the user, you must set mark_messages_deleted to true and provide an additional parameter called hard_delete with the value set to true. This method will delete all messages, reactions, and any other associated data with the user.


const destroy = await client.deleteUser('user_id', {
    mark_messages_deleted: true,
    hard_delete: true,
});
                    


                    


                    


                    


                    


                    

After deleting or hard deleting a user, the user will no longer be able to:

  • Connect to Stream Chat

  • Send or receive messages

  • Be displayed when querying users

  • Have messages stored in Stream Chat (depending on whether or not mark_messages_deleted is set to true or false)

Once a user has been deleted, there is no way to reinstate their access without creating a new user in Stream Chat.

Server-Side API Clients

Stream Chat provides several server-side API clients for ease of use. The following server-side API clients are available to download via their respective package managers:

  1. Python

  2. Node.js

  3. PHP

  4. Go

  5. Ruby

  6. .Net

For a comprehensive list of both server-side and client-side clients, please reference the GitHub repository located here.

Server-Side API Usage

The REST documentation is considered for advanced developers and should only be used when a server-side client is not available. Full, self-documented REST documentation here.

Server-side API Usage

Here’s an example of sending a chat message from a server-side integration:


const options = {};
const serverClient = new StreamChat('STREAM_KEY', 'STREAM_API_SECRET', options);
const spacexChannel = serverClient.channel('team', 'spacex', {
    image: image,
    created_by: elon,
});
const createResponse = await spacexChannel.create();
const text = 'I was gonna get to mars but then I got high';
const message = {
    text,
    user: elon,
}
const response = await spacexChannel.sendMessage(message);
                    

# gem install stream-chat-ruby

require 'stream-chat'

client = StreamChat::Client.new(api_key='STREAM_KEY', api_secret='STREAM_API_SECRET')
spacex_channel = client.channel("team", channel_id: "spacex", data: {'image'=> image})
spacex_channel.create('elon')


spacex_channel.send_message({'text' => 'I was gonna get to mars but then I got high'}, 'elon')
                    

# pip install stream-chat

from stream_chat import StreamChat

client = StreamChat(api_key="STREAM_KEY", api_secret="STREAM_API_SECRET")
spacex_channel = client.channel('team', 'spacex', {
    "image": image,
});
spacex_channel.create("elon")

spacex_channel.send_message(
    {"text": "I was gonna get to mars but then I got high"},
    user_id="elon"
)
                    

// at the moment we don't have a Java client for server side usage
                    

// go get github.com/GetStream/stream-chat-go

import stream "github.com/GetStream/stream-chat-go"

client, _ := stream.NewClient("STREAM_KEY", []byte("STREAM_API_SECRET"))

spacexChannel, _ := client.CreateChannel("team", "spacex","elon", map[string]interface{}{
    "image": image,
})

message := stream.Message{
    Text: "I was gonna get to mars but then I got high",
}

channel.SendMessage(&message, "elon")
                    

// nuget install stream-chat-net

using StreamChat;

var client = new Client("STREAM_KEY", "STREAM_API_SECRET");
var spacexChannel = client.Channel("team", "spacex");
spacexChannel.Create('elon');

var message = new MessageInput()
{
    Text = "I was gonna get to mars but then I got high",
};
spacexChannel. SendMessage(message, 'elon');
                    

// at the moment we don't have a Swift client for server side usage
                    

//composer require get-stream/stream-chat

$client = new GetStream\StreamChat\Client("STREAM_KEY", "STREAM_API_SECRET");
$spacexChannel = $client->Channel("team", "spacex", ['image' => image]);
$spacexChannel->create('elon');

$spacexChannel->sendMessage(["text" => "I was gonna get to mars but then I got high"], 'elon');
                    

Note the 3 differences compared to the client side integration:

  1. Initialize the client with the server side secret.

  2. Create the Channel instead of initializing it (we don't want to retrieve the state or subscribe to changes in this case).

  3. Pass the user parameter if the endpoint requires it (eg. creating a channel or a message)


const tommaso = { id: "tommaso" };

// create a channel and assign user Tommaso as owner
const channel = serverClient.channel("team", "stream", { created_by: tommaso })
await channel.create();

// send a message for tommaso
const message = {
   text: "hi there!",
   user: tommaso,
};
await channel.sendMessage(message);
                    

# create a channel and assign user Tommaso as owner
channel = client.channel("team", "stream")
channel.create("tommaso")

# send a message for tommaso
message = {
    "text": "hi there!",
    "user": "tommaso"
}
channel.send_message(message, "tommaso")
                    

Role Management

Change a User Role


await serverClient.updateUser({
    id: 'tommaso',
    name: 'Tommy Doe',
    role: 'admin',
});
                    

client.update_user({
    "id" => "tommaso",
    "name" => "Tommy Doe",
    "role" => "admin",
});
                    

client.update_user({
    "id": "tommaso",
    "name": "Tommy Doe",
    "role": "admin",
});
                    

client->updateUser([
    "id" => "tommaso",
    "name" => "Tommy Doe",
    "role" => "admin"
]);
                    

// at the moment we don't have a Java client for server side usage
                    

user := User{
    ID: "tommaso",
    Role: "admin",
    ExtraData: map[string]interface{}{
        "name": "Tommy Doe",
    }
};

client.UpdateUser(&user);
                    

// at the moment we don't have a Swift client for server side usage
                    

var user = new User()
{
    ID = "tommaso",
    Role = Role.Admin,
};
bob.SetData("name", "Tommy Doe");

await client.Users.Update(user);
                    

Add Moderators to a Channel


let channel = serverClient.channel("livestream", "fortnite");
await channel.addModerators(["thierry", "tommaso"]));
                    

channel = client.channel("livestream", "fortnite");
channel.add_moderators(["thierry", "tommaso"]);
                    

channel = client.channel("livestream", "fortnite");
channel.add_moderators(["thierry", "tommaso"]);
                    

channel = client->Channel("livestream", "fortnite");
channel->addModerators(["thierry", "tommaso"]);
                    

// at the moment we don't have a Java client for server side usage
                    

channel = client.Channel("livestream", "fortnite");
channel.AddModerators("thierry", "tommaso");
                    

// at the moment we don't have a Swift client for server side usage
                    

var chan = client.Channel("livestream", "fortnite");
await chan.AddModerators(new string[] { "thierry", "tommaso" });
                    

Remove Moderators From a Channel


await channel.demoteModerators(["thierry"]));
                    

channel.demote_moderators(["thierry"]);
                    

channel.demote_moderators(["thierry"]);
                    

channel->demoteModerators(["thierry"]);
                    

// at the moment we don't have a Java client for server side usage
                    

channel.DemoteModerators("thierry");
                    

// at the moment we don't have a Swift client for server side usage
                    

await chan.DemoteModerators(new string[] { "thierry", "tommaso" });
                    

Overview

Message Markup Language (MML) enables you to build interactive messages experiences. See below for an example message using MML:

MML is an experimental feature, contact support to get early access. We're looking for as much feedback as possible about your various use cases prior to the public launch.

MML supports adding buttons to your messages and simple forms with optimized UI for mobile. Date Scheduling, Basic images, icons, and tables are also supported.

The goal for MML is to have a standardized way to handle the most common use cases for message interactivity. If you need something more complex, you can always write a custom message/attachment layout.

As a first example, let's render a simple button:

A Simple MML Example


<mml name="support">
  <text>It looks like your credit card isn't activated yet, activate it now:</text>
  <button name="action" value="Activate">Activate Card</button>
</mml>
                    

MML an attachment type in Stream Chat. You can send a message with MML like this:


const mmlSource = '<mml><button name="action" value="Activate">Activate Card</button></mml>';
const message = {attachments: [{type: 'mml', mml: mmlSource}]};
const response = await channel.sendMessage(message);
                    

Scheduling an Appointment

MML supports a <scheduler> tag and an <add_to_calendar> tag. This makes it easy to schedule an appointment without ever leaving your chat interface.

Here's how you can render a scheduler interface. Note how the interface takes availability into account using an iCal calendar. The UI is optimized for scheduling a date in the near future:


<mml>
  <text>When would you like to schedule your massage?</text>
  <scheduler name="massage_appointment" duration="30" availability_ical="ical_url" />
  <button name="action" value="reserve">Reserve</button>
</mml>
                    

After the user schedules an appointment, you'll want to show an add to calendar widget:


<mml>
  <text>Thanks for scheduling the appointment. Add it to your calendar</text>
  <add_to_calendar title="Massage with Jessica" start="2019-12-24T14:42:54.148Z" end="2019-12-24T15:42:54.148Z" />
</mml>
                    

A Carousel Interface

For many customer support and e-commerce use-cases, you'll want to render a carousel. This type of layout makes it easy to select from a short list of options and show some additional information:


<mml>
    <md>Here are some front bumpers! that will fit your **2018-2019 JL**!</md>
    <carousel>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J107329-JL?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade Adventure HD Front Bumper</text>
            <md>**$404.99**</md>
            <button url="https://www.extremeterrain.com/barricade-adventure-hd-front-bumper-2018-jl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J116651?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade Adventure HD Front Bumper w/ LED Fog Lights &amp; 20 in. LED Light Bar</text>
            <md>**$529.99**</md>
            <button url="https://www.extremeterrain.com/barricade-adventure-hd-front-bumper-w-led-fog-lights-20-led-light-bar-0718-wrangl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J127063-JL?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade HD Front Bumper w/ 20 in. Light Bar</text>
            <md>**$549.99**</md>
            <button url="https://www.extremeterrain.com/barricade-hd-front-bumper-w-20-light-bar-2018-jl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J116311?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>RedRock 4x4 Avenger Full Width Front Bumper w/o Winch Plate</text>
            <md>**$729.99**</md>
            <button url="https://www.extremeterrain.com/redrock-4x4-avenger-full-width-front-bumper-w-o-winch-mount-0718-jk.html">View Product</button>
        </item>
    </carousel>
</mml>
                    

Button List Example

The button list is a convenient way to show a small list of options to a user:


<mml name="food_order">
  Sorry we're currently out of fries, what would you like as a side?
  <button_list>
    <button name="side" value="salad">Salad</button>
    <button name="side" value="potatoes">Baked Potatoes</button>
    <button name="side" value="fried_pickles">Fried Pickles</button>
  </button_list>
</mml>
                    

Number Input

Chat is typically rendered on a mobile mobile device, which inherently has a small interface to work with. The data elements are optimized for mobile input. For example, here is how you can input a number:


<mml name="counts">
  <text>How many donuts do you want for lunch?</text>
  <image src="https://images.unsplash.com/photo-1527904324834-3bda86da6771?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=80" />
  <number name="donuts" value="3" />
  <button name="confirm" value="Confirm">Confirm</button>
</mml>
                    

Tags

The following tags are supported by MML:

Basic Text and Icons

  • text

  • md

  • icon

  • image

Input and Data Tags

  • button

  • button_list

  • scheduler

  • add_to_calendar

  • carousel

  • item

  • number

Layout Tags

  • row

  • column

Basic Tags

Text Tag

The <text> tag is the default tag. It does not support additional attributes and renders the text as-is.

Markdown Tag

The <md> tag renders it's content as markdown.

Icon Tag

The <icon> tag renders a material UI style icon. You can find the list of supported icons here.

Name Type Description Default Optional
name string The icon to render -

Image Tag

The <image> tag supports three options: src, alt, and title.

Here's an example combining all four tags:


<mml name="layout">
  <text>Unsplash has the best pictures</text>
  <image src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=250&q=80" alt="Mountain" title="Mountain" />
  <md>**awesome** picture!</md>
  <icon name="favorite" />
</mml>
                    

Input and data tags

Button Tag

The <button> tag is the most common type of message interaction. Text is the only valid child element. It supports the following parameters:

Name Type Description Default Optional
name string The name of the button -
value string The value of the button -
url string Alternatively you can specify a URL to open -

Button List Tag

The <button_list> tag enables you to render a vertical list of buttons. It's useful if you want the user to choose from a small list of options. The only allowed children of a button_list tag are buttons.

Scheduler Tag

The <scheduler> tag enables your users to schedule an appointment.

Name Type Description Default Optional
duration integer Duration of the appointment in minutes -
ical_availability string The URL to an ICAL calendar with availability for what you're scheduling. Note that we support ICAL recurrence rules. So you can block out certain days etc. -
name string The name for this scheduler element -

Add to Calendar Tag

The <add_to_calendar> tag allows a user to add an appointment to their calendar. It supports Google, Outlook and Apple calendars.

Name Type Description Default Optional
title string The title of the calendar entry -
start string The start date (ISO format) -
end string The end date (ISO format) -
location string The location -
description string A description for the calendar item -

Carousel & Item Tags

The <carousel> tag is an alternative to the button list for showing a list of options to the user. It's typically better suited for a list of articles (e.g. a list of products, etc.). The only children allowed inside of a carousel tag are <item> tags.

Below is an example:


<mml>
    <md>Here are some front bumpers! that will fit your **2018-2019 JL**!</md>
    <carousel>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J107329-JL?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade Adventure HD Front Bumper</text>
            <md>**$404.99**</md>
            <button url="https://www.extremeterrain.com/barricade-adventure-hd-front-bumper-2018-jl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J116651?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade Adventure HD Front Bumper w/ LED Fog Lights &amp; 20 in. LED Light Bar</text>
            <md>**$529.99**</md>
            <button url="https://www.extremeterrain.com/barricade-adventure-hd-front-bumper-w-led-fog-lights-20-led-light-bar-0718-wrangl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J127063-JL?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>Barricade HD Front Bumper w/ 20 in. Light Bar</text>
            <md>**$549.99**</md>
            <button url="https://www.extremeterrain.com/barricade-hd-front-bumper-w-20-light-bar-2018-jl.html">View Product</button>
        </item>
        <item>
            <image src="https://turn5.scene7.com/is/image/Turn5/J116311?wid=250&amp;hei=187&amp;op_usm=0.8,1,10,0" />
            <text>RedRock 4x4 Avenger Full Width Front Bumper w/o Winch Plate</text>
            <md>**$729.99**</md>
            <button url="https://www.extremeterrain.com/redrock-4x4-avenger-full-width-front-bumper-w-o-winch-mount-0718-jk.html">View Product</button>
        </item>
    </carousel>
</mml>
                    

The Number Tag

The <number> tag is a convenient way to have a user select a small number.

Name Type Description Default Optional
name string The name of the number input -
value string The initial value -

Layout Tags

Row & Column Tags

The row and column tags implement a 12 cell grid system. By providing a grid system, MML offers you more fine-grained control over the layout for the user experience. Here's an example:


<mml name="support">
<text>Did you authorize these last 3 transactions on your account?</text>
  <row>
    <column width="4">$15</column>
    <column width="8">Oren's Hummus</column>
  </row>
  <row>
    <column width="4">$1000</column>
    <column width="8">Apple monitor stand</column>
  </row>
  <row>
    <column width="4">$60</column>
    <column width="8">Epic Games Skins</column>
  </row>
<button name="authorized" value="yes">Yes</button>
<button name="authorized" value="yes">No</button>
</mml>
                    

The row only allows tags as its children. The column tag has the following parameters:

Name Type Description Default Optional
width string the width of the cell -
offset string the offset to use -
align string left, center or right -

Audio & Video

Stream has partnered with Voxeet, a division of Dolby Labs. Voxeet integrates seamlessly with Stream Chat and provides robust APIs for audio and video support within your Stream Chat application.

The Voxeet core is built on top of WebRTC, the leading real-time communication standard, so you're free to develop for desktop, mobile, or web without additional plugins or add-ons.

Support

Voxeet's platform provides a suite of APIs for the following use-cases with Stream Chat:

  • 3D Immersive Calls (This helps balance the dynamics of a conference so that all participants can be heard and avoids one person talking over another)

  • Video Calling (Support for HD 720p at 30 frames per second for single or multiple users)

  • Content Sharing (Enables users to present what is on their screen with everybody in the conference – e.g., presentations, whiteboarding, peer review, etc.)

  • Streaming / Broadcasting (Supports both HLS and RTMP streaming for use-cases such as YouTube and Facebook)

  • Recording (Flexibility for customers to create unique recording experiences for their users)

A client-side SDK is provided for ease of use and fast integration with all frameworks. For React Specific integrations, Voxeet delivers a robust set of React Components (UX Kit) that can easily be added to your application alongside Stream Chat.

Full documentation for the Voxeet REST API can be found here. Similar to Stream Chat, Voxeet also provides support for Webhooks.

Example & Tutorial

Stream Chat in combination with Voxeet allows developers to build a rich user experience including two-way video in addition to chat. To show off the various capabilities, our team set out to build a fully functional application in React using the Stream Chat React Components and the Voxeet UX Kit.

If you would like to demo the application, the live URL is located here. The full source code can be found on GitHub (API and Web), and an in-depth tutorial is located on the Stream Blog.