Reactions
Overview
You can react to both activities and comments. It's possible to configure any reaction types that best fit your app.
When adding reactions and the enforce_unique flag is set to true, the existing reaction of a user will be overridden with the new reaction. Use this flag if you want to ensure users have a single reaction per each comment/activity. The default value is false, in which case users can have multiple reactions. The response tells you whether the reaction was new or replaced an existing one — see Reaction write outcome.
// Add a reaction to an activity
_, err = client.Feeds().AddReaction(context.Background(), activityID, &getstream.AddReactionRequest{
Type: "like",
Custom: map[string]any{
"emoji": "❤️",
},
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error adding reaction:", err)
}
// Adding a reaction without triggering push notifications
_, err = client.Feeds().AddReaction(context.Background(), activityID, &getstream.AddReactionRequest{
Type: "like",
Custom: map[string]any{
"emoji": "❤️",
},
UserID: getstream.PtrTo("john"),
SkipPush: getstream.PtrTo(true),
})
if err != nil {
log.Fatal("Error adding reaction without push:", err)
}
// Add a reaction to a comment
_, err = client.Feeds().AddCommentReaction(context.Background(), commentID, &getstream.AddCommentReactionRequest{
Type: "like",
Custom: getstream.PtrTo(map[string]any{
"emoji": "👍",
}),
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error adding comment reaction:", err)
}
// Adding a comment reaction without triggering push notifications
_, err = client.Feeds().AddCommentReaction(context.Background(), commentID, &getstream.AddCommentReactionRequest{
Type: "like",
Custom: getstream.PtrTo(map[string]any{
"emoji": "👍",
}),
UserID: getstream.PtrTo("john"),
SkipPush: getstream.PtrTo(true),
})
if err != nil {
log.Fatal("Error adding comment reaction without push:", err)
}
// Delete activity reaction
_, err = client.Feeds().DeleteActivityReaction(context.Background(), activityID, "like", &getstream.DeleteActivityReactionRequest{
UserID: getstream.PtrTo("john"),
})
if err != nil {
log.Fatal("Error deleting activity reaction:", err)
}Overview of the reaction model
FeedsReactionResponse
| Name | Type | Description | Constraints |
|---|---|---|---|
activity_id | string | - | Required |
comment_id | string | - | - |
created_at | number | - | Required |
custom | object | - | - |
type | string | - | Required |
updated_at | number | - | Required |
user | UserResponse | - | Required |
By default creating a reaction doesn't create an activity. Pass create_notification_activity: true to add a notification to the reacted-to user's notification feed — see Reaction notification creation timing (added June 23, 2026). The response includes notification_accepted and notification_task_id; notification_created is deprecated and mirrors notification_accepted.
Reaction write outcome
Adding a reaction returns what the write actually did, so you can keep your own reaction counters from a single request instead of reading state first or deleting before adding.
| field | type | description |
|---|---|---|
outcome |
string | One of created, replaced, unchanged. What the write did to this user's reaction on the target. |
previous_reaction_type |
string or null |
The reaction type that was replaced. Non-null exactly when outcome is replaced. |
counter_delta |
number | The change to the number of reactions this user holds on the target: 1 or 0. |
outcome |
when | previous_reaction_type |
counter_delta |
|---|---|---|---|
created |
A new reaction was written and nothing was replaced. | null |
1 |
replaced |
enforce_unique removed one or more of the user's other reaction types. |
the removed type | 0 |
unchanged |
The user already held this reaction type. Its custom data may have been updated. |
null |
0 |
replaced only happens with enforce_unique: true — without it nothing is ever removed.
To maintain a per-user reaction counter:
- Adding: increment when
outcomeiscreated, otherwise leave it alone. This is whatcounter_deltaalready gives you. - Deleting: a successful delete always removed a row, so decrement on
2xx. A404means there was nothing to remove. These endpoints never returncounter_delta: -1.
Without enforce_unique a user can hold several reaction types on the same target, so created there means this reaction type was newly added, not the user's first reaction on this target. The two only coincide when enforce_unique: true is set, which is the mode to use if you are counting one reaction per user per target.
Re-sending the same reaction type with enforce_unique: true updates the existing reaction in place and preserves its original created_at.
When you read a feed the reactions are included. Here's an example:
feed := client.Feeds().Feed("user", "john")
feedResponse, err := feed.GetOrCreate(context.Background(), &getstream.GetOrCreateFeedRequest{
UserID: getstream.PtrTo("john"),
})
fmt.Println(feedResponse.Data.Activities[0].OwnReactions)
fmt.Println(feedResponse.Data.Activities[0].LatestReactions)
fmt.Println(feedResponse.Data.Activities[0].ReactionGroups)Querying Reactions
You can query reactions to both activities and comments. Here are some examples:
await client.queryActivityReactions({
activity_id: activity.id,
// Provide optional filters
filter: {
reaction_type: "like",
},
});
await client.queryCommentReactions({
id: comment.id,
// Provide optional filters
filter: {
reaction_type: "like",
},
});Batch query activity reactions
batchQueryActivityReactions fetches a single user's reactions across a whole page of activities in one call, so you can merge own_reactions back onto a cached page without calling query reactions once per activity. Pass up to 100 activity IDs per request. The response contains reaction rows only (no activity payloads) and supports the same filter, sort, and pagination options as the single-activity query.
On a client-side connection the returned reactions are always the connected user's. Server-side, pass user_id to choose whose reactions to fetch.
// Fetch the connected user's reactions across a page of activities in one request.
const response = await client.batchQueryActivityReactions({
activity_ids: activities.map((activity) => activity.id),
// Optional: filter by reaction type, sort, or paginate with next/prev
filter: { reaction_type: "like" },
});
// Merge response.reactions back onto the activities as own_reactions
console.log(response.reactions);Reaction queryable built-in fields
Both batchQueryActivityReactions and batchQueryCommentReactions accept these filter fields.
| name | type | description | supported operations | example |
|---|---|---|---|---|
reaction_type |
string or list of strings | The type of reaction | $in, $eq |
{ reaction_type: { $in: [ 'like', 'heart', 'thumbs_up' ] } } |
user_id |
string or list of strings | The ID of the user who created the reaction | $in, $eq |
{ user_id: { $eq: 'user_123' } } |
created_at |
string, must be formatted as an RFC3339 timestamp | The time the reaction was created | $eq, $gt, $gte, $lt, $lte |
{ created_at: { $gte: '2023-12-04T09:30:20.45Z' } } |
Batch query comment reactions
batchQueryCommentReactions is the comment counterpart of batch query activity reactions. It fetches a single user's reactions across up to 100 comments in one call, returning reaction rows only (no comment payloads). Use it to hydrate own_reactions for a whole comment thread without a request per comment. It accepts the same filter fields, sort, and pagination options.
As with activities, a client-side connection returns the connected user's reactions; server-side, pass user_id.
const response = await client.batchQueryCommentReactions({
comment_ids: comments.map((comment) => comment.id),
filter: { reaction_type: "like" },
});
console.log(response.reactions);