Prerequisites
Setup
1
Get your Stream Chat credentials
In Stream, open the dashboard, select your app, and copy the API key and API secret.
2
Configure in Courier
Open the in Courier, enter your API key, API secret, and sender ID, then save.
Profile requirements
Stream Chat posts a new message to a channel, or updates one that already exists. The profile you send to needs astreamChat object for whichever you want. Store it once with , which merges into the profile and creates it if it does not exist:
- New message
- Update a message
channelType and channelId name the channel to post into.const profile = await client.profiles.create('user_123', {
profile: {
streamChat: {
channelType: 'messaging',
channelId: 'my-channel-id',
},
},
});
profile = client.profiles.create(
user_id="user_123",
profile={
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id",
},
},
)
curl --request POST \
--url https://api.courier.com/profiles/user_123 \
--header "Authorization: Bearer $COURIER_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"profile": {
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
}
}'
profile = courier.profiles.create(
"user_123",
profile: {
streamChat: {
channelType: "messaging",
channelId: "my-channel-id"
}
}
)
profile, err := client.Profiles.New(
context.TODO(),
"user_123",
courier.ProfileNewParams{
Profile: map[string]any{
"streamChat": map[string]any{
"channelType": "messaging",
"channelId": "my-channel-id",
},
},
},
)
ProfileCreateParams params = ProfileCreateParams.builder()
.userId("user_123")
.profile(ProfileCreateParams.Profile.builder()
.putAdditionalProperty("streamChat", JsonValue.from(java.util.Map.of(
"channelType", "messaging",
"channelId", "my-channel-id")))
.build())
.build();
ProfileCreateResponse profile = client.profiles().create(params);
$profile = $client->profiles->create('user_123', profile: [
'streamChat' => [
'channelType' => 'messaging',
'channelId' => 'my-channel-id',
],
]);
ProfileCreateParams parameters = new()
{
UserID = "user_123",
Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
}
"""
),
};
var profile = await client.Profiles.Create(parameters);
courier profiles create \
--api-key "$COURIER_API_KEY" \
--user-id user_123 \
--profile '{"streamChat":{"channelType":"messaging","channelId":"my-channel-id"}}'
With Courier MCP, save the Stream Chat messaging channel my-channel-id on user_123.
messageId replaces the content of a message already posted.const profile = await client.profiles.create('user_123', {
profile: {
streamChat: {
messageId: 'my-message-id',
},
},
});
profile = client.profiles.create(
user_id="user_123",
profile={
"streamChat": {
"messageId": "my-message-id",
},
},
)
curl --request POST \
--url https://api.courier.com/profiles/user_123 \
--header "Authorization: Bearer $COURIER_API_KEY" \
--header 'Content-Type: application/json' \
--data '{
"profile": {
"streamChat": {
"messageId": "my-message-id"
}
}
}'
profile = courier.profiles.create(
"user_123",
profile: {
streamChat: {
messageId: "my-message-id"
}
}
)
profile, err := client.Profiles.New(
context.TODO(),
"user_123",
courier.ProfileNewParams{
Profile: map[string]any{
"streamChat": map[string]any{
"messageId": "my-message-id",
},
},
},
)
ProfileCreateParams params = ProfileCreateParams.builder()
.userId("user_123")
.profile(ProfileCreateParams.Profile.builder()
.putAdditionalProperty("streamChat", JsonValue.from(java.util.Map.of(
"messageId", "my-message-id")))
.build())
.build();
ProfileCreateResponse profile = client.profiles().create(params);
$profile = $client->profiles->create('user_123', profile: [
'streamChat' => [
'messageId' => 'my-message-id',
],
]);
ProfileCreateParams parameters = new()
{
UserID = "user_123",
Profile = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"streamChat": {
"messageId": "my-message-id"
}
}
"""
),
};
var profile = await client.Profiles.Create(parameters);
courier profiles create \
--api-key "$COURIER_API_KEY" \
--user-id user_123 \
--profile '{"streamChat":{"messageId":"my-message-id"}}'
With Courier MCP, save the Stream Chat message id my-message-id on user_123.
user_id and Courier resolves the address.
For a one-off with no stored profile, pass it inline instead: "to": { "streamChat": { "channelType": "messaging", "channelId": "my-channel-id" } }.
Send by user id
The call in every language, and the rest of the profile object.
Send to a recipient
- Send to user id
- Send to
streamChat
Courier reads
streamChat off the saved profile, so preferences apply and the value can change without touching this code.const { requestId } = await courier.send.message({
message: {
to: {
user_id: "user_123",
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
},
});
response = client.send.message(
message={
"to": {
"user_id": "user_123",
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
},
)
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": {
"user_id": "user_123"
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40"
}
}'
response = courier.send_.message(
message: {
to: {
user_id: "user_123"
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40"
}
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{
OfUserRecipient: &shared.UserRecipientParam{
UserID: courier.String("user_123"),
},
},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(UserRecipient.builder().userId("user_123").build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'to' => [
'user_id' => 'user_123',
],
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
],
);
SendMessageParams parameters = new()
{
Message = new()
{
To = new UserRecipient { UserID = "user_123" },
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"user_id": "user_123"}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40
With Courier MCP, send my template to user_123 by email.
Pass it inline instead and nothing is stored. Swap this
to object into the call on the other tab.const { requestId } = await courier.send.message({
message: {
// A provider recipient is a profile field, so it sits outside the typed union.
to: {
streamChat: {
channelType: "messaging",
channelId: "my-channel-id",
},
} as any,
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
},
});
response = client.send.message(
message={
"to": {
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id",
},
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
},
)
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"to": {
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
},
"template": "nt_01kx4h2jdafq8bk9aftxak4b40"
}
}'
response = courier.send_.message(
message: {
to: {
streamChat: {
channelType: "messaging",
channelId: "my-channel-id"
}
},
template: "nt_01kx4h2jdafq8bk9aftxak4b40"
}
)
// A provider recipient is a profile field, so it sits outside the typed union.
to := param.Override[shared.UserRecipientParam](
json.RawMessage(`{"streamChat": {"channelType": "messaging", "channelId": "my-channel-id"}}`),
)
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
},
})
// A provider recipient is a profile field, so it sits outside the typed union.
UserRecipient to = UserRecipient.builder()
.putAdditionalProperty("streamChat", JsonValue.from(Map.of(
"channelType", "messaging",
"channelId", "my-channel-id")))
.build();
client.send().message(SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
.to(to)
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.build())
.build());
$response = $client->send->message(
message: [
'to' => [
'streamChat' => [
'channelType' => 'messaging',
'channelId' => 'my-channel-id',
],
],
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
],
);
// A provider recipient is a profile field, so it sits outside the typed union.
UserRecipient to = UserRecipient.FromRawUnchecked(
JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""{"streamChat": {"channelType": "messaging", "channelId": "my-channel-id"}}"""
)
);
var response = await client.Send.Message(new() { Message = new() { To = to, Template = "nt_01kx4h2jdafq8bk9aftxak4b40" } });
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"streamChat": {"channelType": "messaging", "channelId": "my-channel-id"}}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40
With Courier MCP, send my template to the Stream Chat channel my-channel-id.
Overrides
covers the two levels and which one wins. A provider override changes the request body, the API credentials, and the endpoint URL Courier sends to Stream Chat.| Field | Sets |
|---|---|
body | Any field Stream Chat’s SendMessage endpoint accepts at POST /channels/{type}/{id}/message. |
config | apiKey, apiSecret, senderId, and baseUrl, which defaults to https://chat-us-east-1.stream-io-api.com. |
const { requestId } = await courier.send.message({
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
streamChat: {
channelType: "messaging",
channelId: "my-channel-id",
},
},
data: {
name: "Sarah Bennett",
},
providers: {
"stream-chat": {
override: {
body: {
skip_push: true,
},
config: {
baseUrl: "https://custom-stream-endpoint.example.com",
apiKey: "RUNTIME_API_KEY",
apiSecret: "RUNTIME_API_SECRET",
senderId: "RUNTIME_SENDER_ID",
},
},
},
},
},
});
response = client.send.message(
message={
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id",
},
},
"data": {
"name": "Sarah Bennett",
},
"providers": {
"stream-chat": {
"override": {
"body": {
"skip_push": True,
},
"config": {
"baseUrl": "https://custom-stream-endpoint.example.com",
"apiKey": "RUNTIME_API_KEY",
"apiSecret": "RUNTIME_API_SECRET",
"senderId": "RUNTIME_SENDER_ID",
},
},
},
},
},
)
curl -X POST https://api.courier.com/send \
-H "Authorization: Bearer $COURIER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": {
"template": "nt_01kx4h2jdafq8bk9aftxak4b40",
"to": {
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
},
"data": {
"name": "Sarah Bennett"
},
"providers": {
"stream-chat": {
"override": {
"body": {
"skip_push": true
},
"config": {
"baseUrl": "https://custom-stream-endpoint.example.com",
"apiKey": "RUNTIME_API_KEY",
"apiSecret": "RUNTIME_API_SECRET",
"senderId": "RUNTIME_SENDER_ID"
}
}
}
}
}
}'
response = courier.send_.message(
message: {
template: "nt_01kx4h2jdafq8bk9aftxak4b40",
to: {
streamChat: {
channelType: "messaging",
channelId: "my-channel-id"
}
},
data: {
name: "Sarah Bennett"
},
providers: {
"stream-chat": {
override: {
body: {
skip_push: true
},
config: {
baseUrl: "https://custom-stream-endpoint.example.com",
apiKey: "RUNTIME_API_KEY",
apiSecret: "RUNTIME_API_SECRET",
senderId: "RUNTIME_SENDER_ID"
}
}
}
}
}
)
// This provider addresses the recipient with fields outside the typed
// UserRecipient model, so pass the recipient as raw JSON.
to := param.Override[shared.UserRecipientParam](json.RawMessage(`{
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
}`))
response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
Message: courier.SendMessageParamsMessage{
To: courier.SendMessageParamsMessageToUnion{OfUserRecipient: &to},
Template: courier.String("nt_01kx4h2jdafq8bk9aftxak4b40"),
Data: map[string]any{
"name": "Sarah Bennett",
},
Providers: shared.MessageProvidersParam{
"stream-chat": shared.MessageProvidersTypeParam{
Override: map[string]any{
"body": map[string]any{
"skip_push": true,
},
"config": map[string]any{
"baseUrl": "https://custom-stream-endpoint.example.com",
"apiKey": "RUNTIME_API_KEY",
"apiSecret": "RUNTIME_API_SECRET",
"senderId": "RUNTIME_SENDER_ID",
},
},
},
},
},
})
SendMessageParams params = SendMessageParams.builder()
.message(SendMessageParams.Message.builder()
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so pass them as additional properties.
.to(UserRecipient.builder()
.putAdditionalProperty("streamChat", JsonValue.from(java.util.Map.of(
"channelType", "messaging",
"channelId", "my-channel-id"
)))
.build())
.template("nt_01kx4h2jdafq8bk9aftxak4b40")
.data(JsonValue.from(java.util.Map.of(
"name", "Sarah Bennett"
)))
.providers(MessageProviders.builder()
.putAdditionalProperty("stream-chat", JsonValue.from(java.util.Map.of("override", java.util.Map.of(
"body", java.util.Map.of(
"skip_push", true
),
"config", java.util.Map.of(
"baseUrl", "https://custom-stream-endpoint.example.com",
"apiKey", "RUNTIME_API_KEY",
"apiSecret", "RUNTIME_API_SECRET",
"senderId", "RUNTIME_SENDER_ID"
)
))))
.build())
.build())
.build();
SendMessageResponse response = client.send().message(params);
$response = $client->send->message(
message: [
'template' => 'nt_01kx4h2jdafq8bk9aftxak4b40',
'to' => [
'streamChat' => [
'channelType' => 'messaging',
'channelId' => 'my-channel-id',
],
],
'data' => [
'name' => 'Sarah Bennett',
],
'providers' => [
'stream-chat' => [
'override' => [
'body' => [
'skip_push' => true,
],
'config' => [
'baseUrl' => 'https://custom-stream-endpoint.example.com',
'apiKey' => 'RUNTIME_API_KEY',
'apiSecret' => 'RUNTIME_API_SECRET',
'senderId' => 'RUNTIME_SENDER_ID',
],
],
],
],
],
);
SendMessageParams parameters = new()
{
Message = new()
{
// This provider addresses the recipient with fields outside the
// typed UserRecipient model, so build it from raw JSON.
To = UserRecipient.FromRawUnchecked(
JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"streamChat": {
"channelType": "messaging",
"channelId": "my-channel-id"
}
}
"""
)
),
Template = "nt_01kx4h2jdafq8bk9aftxak4b40",
Data = new Dictionary<string, JsonElement>()
{
{ "name", JsonSerializer.SerializeToElement("Sarah Bennett") },
},
Providers = new Dictionary<string, MessageProvidersType>()
{
{
"stream-chat",
new()
{
Override = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
"""
{
"body": {
"skip_push": true
},
"config": {
"baseUrl": "https://custom-stream-endpoint.example.com",
"apiKey": "RUNTIME_API_KEY",
"apiSecret": "RUNTIME_API_SECRET",
"senderId": "RUNTIME_SENDER_ID"
}
}
"""
),
}
},
},
},
};
var response = await client.Send.Message(parameters);
courier send message \
--api-key "$COURIER_API_KEY" \
--message.to '{"streamChat": {"channelType": "messaging", "channelId": "my-channel-id"}}' \
--message.template nt_01kx4h2jdafq8bk9aftxak4b40 \
--message.data '{"name": "Sarah Bennett"}' \
--message.providers '{"stream-chat": {"override": {"body": {"skip_push": true}, "config": {"baseUrl": "https://custom-stream-endpoint.example.com", "apiKey": "RUNTIME_API_KEY", "apiSecret": "RUNTIME_API_SECRET", "senderId": "RUNTIME_SENDER_ID"}}}}'
With Courier MCP, send my template to this Stream Chat channel with a different API key and sender.
Provider details
stream-chat
routing.channels instead is supported, and sends through just this provider.
Send to a specific provider
When that is worth doing, and what you give up: failover, channel priority, and providers you add later.