Scalable news feeds & activity streams with Node/Javascript

Learn how easy it can be to build scalable newsfeeds and activity streams

Stream has official clients for JS/Node, Ruby, Python, PHP, Go and Java. There are framework integrations available for Rails, Django, Laravel, Doctrine, Zend and Node. In addition to the official clients, the community has built clients for .NET and Scala.

Client libraries & Framework Integrations

Official Clients

Official clients are currently available for a number of languages:

Community Clients

The community also contributed clients for Scala and Elixir:

If you want to build your own client, head over to the REST API documentation.

Framework Integrations

Stream makes it a breeze to integrate with a number of popular frameworks. If you're new to Stream we recommend starting out with the lower level clients.

Community Framework Projects

The community also contributed the following example projects:

Set up your client

  • Ruby

Let's get set up! First, install the client as specified below:

# install directly with gem
gem install "stream-ruby"

# or add this to your Gemfile and then run bundler
gem "stream-ruby"
// install via npm
npm install getstream

// or yarn
yarn add getstream

// or download the UMD built version from Github
https://raw.githubusercontent.com/GetStream/stream-js/master/dist/js/getstream.js
// pip install stream-python
// install using composer
composer require get-stream/stream
// Add the following dependency and repository to your pom.xml file

<dependency>
    <groupId>io.getstream.client</groupId>
    <artifactId>stream-java</artifactId>
    <version>3.0.0</version>
</dependency>
// install from the command line
go get gopkg.in/GetStream/stream-go2.v1

// import it in your code
import "gopkg.in/GetStream/stream-go2.v1"
// install via Nuget

nuget install stream-net
Note: Source code can be found on GitHub.

To instantiate the client you need an API key and secret. You can find the key and secret on the dashboard.

You're currently not logged in. Quickly register using GitHub to get your API key.
# Instantiate a new client
require 'stream'
client = Stream::Client.new('YOUR_API_KEY', 'API_KEY_SECRET', :location => 'us-east')
# Find your API keys here https://getstream.io/dashboard/
var stream = require('getstream');
// Instantiate a new client (server side)
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', 'APP_ID');
# Instantiate a new client
import stream
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
# Find your API keys here https://getstream.io/dashboard/
// Instantiate a new client
$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');
// Find your API keys here https://getstream.io/dashboard/
// import io.getstream.client.Client;

Client client = Client.builder("YOUR_API_KEY", "API_KEY_SECRET").build();
import (
    stream "gopkg.in/GetStream/stream-go2.v1"
)

client, err := stream.NewClient(
    "YOUR_API_KEY",
    "API_KEY_SECRET",
)
// Instantiate a new client
var client = new StreamClient("YOUR_API_KEY","API_KEY_SECRET");

If you want to use Stream on your mobile or web application, you need to generate a token server-side that the JS client can use to authenticate as a user of your application.

Generate user token server-side

This code generates the token for one of your users; a common place to do this is at signup or login. The token is then passed to the frontend.
user_token = client.create_user_session_token('the-user-id')
const userToken = client.createUserToken('the-user-id');
user_token = client.create_user_token('the-user-id')
$userToken = client->createUserSessionToken("the-user-id");
Token userToken = client.frontendToken("the-user-id");
var userToken = client.CreateUserSessionToken("the-user-id");

Use Stream API client-side (JS)

var stream = require('getstream');

// Instantiate new client with a user token
client = stream.connect('YOUR_API_KEY', userToken,  'APP_ID');

JavaScript Asynchronous methods

All methods that perform an API call do this asynchronous; because of this, these methods do not return their result directly but return a Promise object. For a demonstration of how to use Promises see Adding & Removing Activities. Do not forget to add a catch handler, otherwise API errors will be ingested by the Promise.

Old style callbacks are still supported by the client but Promises are the recommended way of implementing asynchronous computations.

Quick start

The quickstart below shows you how to build a scalable social network. It higlights the most common API calls:

chris = client.feed('user', 'chris')

# Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
activity_data = { :actor => 'chris', :verb => 'add', :object => 'picture:10', :foreign_id => 'picture:10', :message => 'Beautiful bird!' }
chris.add_activity(activity_data);

# Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
jack = client.feed('timeline', 'jack')
jack.follow('user', 'chris')

# Read Jack's timeline and Chris' post appears in the feed:
activities = jack.get(:limit => 10)

# Remove an Activity by referencing it's foreign_id
chris.remove_activity('picture:10', foreign_id=true)
var chris = client.feed('user', 'chris');

// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
chris.addActivity({
  actor: 'chris',
  verb: 'add',
  object: 'picture:10',
  foreign_id: 'picture:10',
  message: 'Beautiful bird!'
}).then(
  null, // nothing further to do
  function(err) {
    // Handle or raise the Error.
  }
);

// Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
var jack = client.feed('timeline', 'jack');
jack.follow('user', 'chris').then(
  null, // nothing further to do
  function(err) {
    // Handle or raise the Error.
  }
);


// Read Jack's timeline and Chris' post appears in the feed:
jack.get({ limit: 10 }).then(function(results) {
  var activityData = results; // work with the feed activities
},function(err) {
    // Handle or raise the Error.
});

// Remove an Activity by referencing it's Foreign Id:
chris.removeActivity({ foreignId: 'picture:10' }).then(
  null, // nothing further to do
  function(err) {
    // Handle or raise the Error.
  }
);
chris = client.feed("user", "chris")

# Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
chris.add_activity({
  "actor": "chris",
  "verb": "add",
  "object": "picture:10",
  "foreign_id": "picture:10",
  "message": "Beautiful bird!"
})

# Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
jack = client.feed("timeline", "jack")
jack.follow('user', "chris")

# Read Jack's timeline and Chris' post appears in the feed:
activities = jack.get(limit=10)["results"]

# Remove an Activity by referencing it's foreign_id
chris.remove_activity(foreign_id="picture:10")
$chris = $client->feed('user', 'chris');

// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
$data = [
  "actor" => "chris",
  "verb" => "add",
  "object" => "picture:10",
  "foreign_id" => "picture:10",
  "message" => "Beautiful bird!",
];

$chris->addActivity($data);

// Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
$jack = $client->feed('timeline', 'jack');
$jack->followFeed('user', 'chris');

// Read Jack's timeline and Chris' post appears in the feed:
$activities = $jack->getActivities(10)['results'];

// Remove the activity by referencing the foreign_id you provided:
$chris->removeActivity("picture:10", true);
FlatFeed chris = client.flatFeed("user", "chris");
// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
chris.addActivity(Activity.builder()
        .actor("chris")
        .verb("add")
        .object("picture:10")
        .foreignID("picture:10")
        .extraField("message", "Beautiful bird!")
        .build());

// Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
FlatFeed jack = client.flatFeed("timeline", "jack");
ack.follow(chris).join();

// Read Jack's timeline and Chris' post appears in the feed:
List<Activity> response = jack.getActivities(new Pagination().limit(10)).join();
for (Activity activity : response) {
        // ...
}

// Remove an Activity by referencing it's foreign_id
chris.removeActivityByForeignID("picture:10").join();
chris := client.FlatFeed("user", "chris")

// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
_, err := chris.AddActivity(stream.Activity{
    Actor:     "chris",
    Verb:      "add",
    Object:    "picture:10",
    ForeignID: "picture:10",
    Extra: map[string]interface{}{
        "message": "Beautiful bird!",
    },
})
if err != nil {
    panic(err)
}

// Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
jack := client.FlatFeed("timeline", "jack")
err = jack.Follow(chris)
if err != nil {
    panic(err)
}

// Read Jack's timeline and Chris' post appears in the feed:
resp, err := jack.GetActivities(stream.WithActivitiesLimit(10))
if err != nil {
    panic(err)
}
for _, activity := range resp.Results {
    // ...
}

// Remove an Activity by referencing it's foreign_id
err = chris.RemoveActivityByForeignID("picture:10")
if err != nil {
    panic(err)
}
var chrisFeed = client.Feed("user", "chris");

// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
var activity = new Activity("chris", "add", "picture:10")
{
    ForeignId = "picture:10"
};
activity.SetData("message", "Beautiful bird!");

chrisFeed.AddActivity(activity);

// Create a following relationship between Jack's "timeline" feed and Chris' "user" feed:
jackTimeline = client.Feed("timeline", "jack");
jackTimeline. FollowFeed("user", "chris");

// Read Jack's timeline and Chris' post appears in the feed:
var activities = await jackTimeline.GetActivities(0, 10);

// Remove the activity by referencing the foreign_id you provided:
chrisFeed.RemoveActivity("picture:10", true);

That was a lot of information all at once. The getting started provides a more detailed and interactive explanation. Keep on scrolling to read the full documentation.

Adding Activities

"In its simplest form, an activity consists of an actor, a verb, and an object. It tells the story of a person performing an action on or with an object."

Adding Activities: Basic

Adding an activity in it’s simplest form means passing an object with the following basic properties:

  • Actor
  • Verb
  • Object
  • Recommended:
    • Foreign Id
    • Time

Here's an example:

“Erik is pinning Hawaii to his Places to Visit board.”

Let's break the example down:

  • Actor: "Eric" (User:1)
  • Verb: "pin"
  • Object: "Hawaii" (Place:42)
  • Foreign Id: "Eric's board activity" (Activity:1)
  • Time: 2017-07-01T20:30:45.123

Now, let's show you how to add an activity to a feed using your Stream API client:

# Instantiate a feed object
user_feed_1 = client.feed('user', '1')
# Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
activity_data = {:actor => "User:1", :verb => "pin", :object => "Place:42"}
activity_response = user_feed_1.add_activity(activity_data)
// Server-side: Instantiate a feed using feed class 'user' and user id '1'
var user1 = client.feed('user', '1');

// Client-side: Instantiate a feed for feed group 'user', user id '1' and a security token generated server side
var user1 = client.feed('user', '1', $token);

// Add an activity to the feed
var activity = {actor: 'User:1', verb: 'pin', object: 'Place:42'};

// Asynchronous methods return Promise since v3.0.0
user1.addActivity(activity)
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure, reason.error contains an explanation */ });
# Instantiate a feed object
user_feed_1 = client.feed('user', '1')

# Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
activity_data = {"actor": "User:1", "verb": "pin", "object": "Place:42"}
activity_response = user_feed_1.add_activity(activity_data)
// Instantiate a feed object
$userFeed1 = $client->feed('user', '1');

// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
$data = [
    "actor"=>"User:1",
    "verb"=>"pin",
    "object"=>"Place:42"
];

$userFeed1->addActivity($data);
// Instantiate a feed object
FlatFeed userFeed = client.flatFeed("user", "1");

// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
Activity activity = Activity.builder()
        .actor("User:1")
        .verb("pin")
        .object("Place:42")
        .target("Board:1")
        .build();
userFeed.addActivity(activity).get();
// Instantiate a feed object
userFeed := client.FlatFeed("user", "1")

// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
activity := stream.Activity{
    Actor:  "User:1",
    Verb:   "pin",
    Object: "Place:42",
}
_, err := userFeed.AddActivity(activity)
if err != nil {
    panic(err)
}
// Instantiate a feed object
var userFeed1 = client.Feed("user", "1");

// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)

var activity = new Activity("User:1", "pin", "Place:42");
userFeed1.AddActivity(activity);

Listed below are the mandatory and recommended fields when adding Activities.

Fields

Name Type Description Default Optional
actor string The actor performing the activity
verb string The verb of the activity with a maximum length of 20 characters
object string The object of the activity
time string The time of the activity, isoformat (UTC localtime). Required to ensure activity uniqueness and also to later update activities by Time + Foreign Id. Current time
to list See the documentation on Targeting & "TO" support.
foreign_id string A unique ID from your application for this activity. IE: pin:1 or like:300. Required to later update activities by Time + Foreign Id.
* string/list/object/point Add as many custom fields as needed

Custom fields

In addition to the mandatory and recommended fields, extra custom fields may be added to an activity. Let's look at a more expressive example:

# Create a bit more complex activity
activity_data = {:actor => 'User:1', :verb => 'run', :object => 'Exercise:42',
    :course => {:name => 'Golden Gate park', :distance => 10},
    :participants => ['Thierry', 'Tommaso'],
    :started_at => DateTime.now(),
    :foreign_id => 'run:1',
    :location => {:type => 'point', :coordinates => [37.769722,-122.476944] }
}
activity_response = user_feed_1.add_activity(activity_data)
// Create a bit more complex activity
activity = {'actor': 'User:1', 'verb': 'run', 'object': 'Exercise:42',
    'course': {'name': 'Golden Gate park', 'distance': 10},
    'participants': ['Thierry', 'Tommaso'],
    'started_at': new Date(),
    'foreign_id': 'run:1',
    'location': {'type': 'point', 'coordinates': [37.769722,-122.476944] }
};
user1.addActivity(activity)
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });
import datetime

# Create a bit more complex activity
activity_data = {'actor': 'User:1', 'verb': 'run', 'object': 'Exercise:42',
    'course': {'name': 'Golden Gate park', 'distance': 10},
    'participants': ['Thierry', 'Tommaso'],
    'started_at': datetime.datetime.utcnow(),
    'foreign_id': 'run:1',
    'location': {'type': 'point', 'coordinates': [37.769722,-122.476944] }
}
user_feed_1.add_activity(activity_data)
// Create a bit more complex activity
$now = new DateTime('now');

$data = [
    'actor' => 'User:1',
    'verb' => 'run',
    'object' => 1,
    'course' => ['name'=> 'Golden Gate park', 'distance'=> 10],
    'participants' => ['Thierry', 'Tommaso'],
    'started_at' => $now,
    'foreign_id' => 'run:1',
    'location' => [
        'type'=> 'point',
        'coordinates'=> [
            37.769722, -122.476944,
        ],
    ],
];

$userFeed1->addActivity($data);
// Create a bit more complex activity
Activity activity = Activity.builder()
        .actor("User:1")
        .verb("run")
        .object("Exercise:42")
        .foreignID("run:1")
        .extra(new ImmutableMap.Builder<String, Object>()
                .put("course", new ImmutableMap.Builder<String, Object>()
                        .put("name", "Golden Gate park")
                        .put("distance", 10)
                        .build())
                .put("participants", new String[]{
                        "Thierry",
                        "Tommaso",
                })
                .put("started_at", LocalDateTime.now())
                .put("location", new ImmutableMap.Builder<String, Object>()
                        .put("type", "point")
                        .put("coordinates", new double[]{37.769722, -122.476944})
                        .build())
                .build())
        .build();
userFeed.addActivity(activity).join();
// Create a bit more complex activity
activity := stream.Activity{
    Actor:     "User:1",
    Verb:      "run",
    Object:    "Exercise:42",
    ForeignID: "run:1",
    Extra: map[string]interface{}{
        "course": map[string]interface{}{
            "name":     "Golden Gate park",
            "distance": 10,
        },
        "participants": []string{
            "Thierry",
            "Tommaso",
        },
        "started_at": time.Now(),
    },
}
_, err := userFeed.AddActivity(activity)
if err != nil {
    panic(err)
}
// Create a bit more complex activity
var activity = new Activity("User:1", "run", "Exercise:42")
{
	ForeignId = "run:1"
};
var course = new Dictionary<string, object>();
course["name"] = "Shevlin Park";
course["distance"] = 10;

var participants = new string[] { "Thierry", "Tommaso" };

var location = new Dictionary<string, object>();
location.Add("type", "point");
location.Add("coordinates", new float[] {37.769722F, -122.476944F});

activity.SetData("location", location);
activity.SetData("course", course);
activity.SetData("participants", participants);

In the above example, the course, location, participants and started_at fields will be stored with the Activity and included whenever the Activity is retrieved.

For performance reasons activities are limited in size (128KB) and must not contain blob/binary data (eg. base64 encoded images). Use references and identifiers to facilitate Activity enrichment by your backend or client.

The pre-defined activity fields listed above are described within the Go client library. Additional custom activity metadata can be inserted using the MetaData property which is a map[string]string type.

These reserved words must not be used as field names: activity_id, activity, analytics, extra_context, id, is_read, is_seen, origin, score, site_id, to.

Foreign IDs

The example above also specified a foreign_id.

The foreign id is a unique identifier for the activity that can be stored and used within the app. Making use of the the foreign id field is highly recommended as it is needed in order to update Activities.

Add Activity response data

When an activity is successfully added, the Stream API includes activity id in the serialized JSON response:

{  
   id:"ef696c12-69ab-11e4-8080-80003644b625",
   actor:"User:1",
   course:{  
      distance:10,
      name:"Golden Gate Park"
   },
   object:"Exercise:42",
   participants:[  
      "Thierry",
      "Tommaso"
   ],
   started_at:"2014-11-11T15:06:16+01:00",
   target:null,
   time:"2014-11-11T14:06:30.494",
   verb:"run"
}
{  
   id:"ef696c12-69ab-11e4-8080-80003644b625",
   actor:"User:1",
   course:{  
      distance:10,
      name:"Golden Gate Park"
   },
   object:"Exercise:42",
   participants:[  
      "Thierry",
      "Tommaso"
   ],
   started_at:"2014-11-11T15:06:16+01:00",
   target:null,
   time:"2014-11-11T14:06:30.494",
   verb:"run"
}
{  
   id:"ef696c12-69ab-11e4-8080-80003644b625",
   actor:"User:1",
   course:{  
      distance:10,
      name:"Golden Gate Park"
   },
   object:"Exercise:42",
   participants:[  
      "Thierry",
      "Tommaso"
   ],
   started_at:"2014-11-11T15:06:16+01:00",
   target:null,
   time:"2014-11-11T14:06:30.494",
   verb:"run"
}
{  
   "id": "ef696c12-69ab-11e4-8080-80003644b625",
   "actor": "User:1",
   "course": {
      "distance":10,
      "name":"Golden Gate Park"
   },
   "object": "Exercise:42",
   "participants": [
      "Thierry",
      "Tommaso"
   ],
   "started_at": "2014-11-11T15:06:16+01:00",
   "target": null,
   "time": "2014-11-11T14:06:30.494",
   "verb": "run"
}
{  
   id:"ef696c12-69ab-11e4-8080-80003644b625",
   actor:"User:1",
   course:{  
      distance:10,
      name:"Golden Gate Park"
   },
   object:"Exercise:42",
   participants:[  
      "Thierry",
      "Tommaso"
   ],
   started_at:"2014-11-11T15:06:16+01:00",
   target:null,
   time:"2014-11-11T14:06:30.494",
   verb:"run"
}
{  
   id:"ef696c12-69ab-11e4-8080-80003644b625",
   actor:"User:1",
   course:{  
      distance:10,
      name:"Golden Gate Park"
   },
   object:"Exercise:42",
   participants:[  
      "Thierry",
      "Tommaso"
   ],
   started_at:"2014-11-11T15:06:16+01:00",
   target:null,
   time:"2014-11-11T14:06:30.494",
   verb:"run"
}

Retrieving Activities

The example below shows how to retrieve the Activities in a feed.

# Get activities from 5 to 10
result = user_feed_1.get(:limit=>5, :offset=>5)

# Get the 5 activities added after last_activity 
result = user_feed_1.get(:limit=>5, :id_lt=>last_activity.id)

# Get the 5 activities added before last_activity 
result = user_feed_1.get(:limit=>5, :id_gt=>last_activity.id)

# Get activities sorted by rank (Ranked Feeds Enabled):
result = user_feed_1.get(:limit=>5, :ranking=>'popularity')
/*
 * Get activities from 5 to 10
 */
user1.get({ limit:5, offset:5 })
  .then(activitiesSuccess)
  .catch(activitiesError);

/*
 * Get the 5 activities added after lastActivity 
 */
user1.get({ limit:5, id_lt: lastActivity.id })
  .then(activitiesSuccess)
  .catch(activitiesError);

/*
 * Get the 5 activities added before lastActivity 
 */
user1.get({ limit:5, id_gt: lastActivity.id })
  .then(activitiesSuccess)
  .catch(activitiesError);

/*
 * Get activities sorted by rank (Ranked Feeds Enabled)
 */
user1.get({ limit:20, ranking:'popularity'})
  .then(activitiesSuccess)
  .catch(activitiesError);

/*
 * Get the 5 activities and enrich them with reactions and collections
 */
user1.get({ 
  limit:20, enrich:true,
  reactions: {own: true, counts: true, recent: true},
})
  .then(activitiesSuccess)
  .catch(activitiesError);

function activitiesSuccess(successData) {
  console.log(successData);
}

function activitiesError(errorData) {
  console.log(errorData);
}
# Get activities from 5 to 10
result = user_feed_1.get(limit=5, offset=5)

# Get the 5 activities added after last_activity 
result = user_feed_1.get(limit=5, id_lt=last_activity.id)

# Get the 5 activities added before last_activity 
result = user_feed_1.get(limit=5, id_gt=last_activity.id)

# Get activities sorted by rank (Ranked Feeds Enabled):
result = user_feed_1.get(limit=5, ranking="popularity")

# Get first 5 activities and enrich them with reactions and collections
result = user_feed_1.get(
    limit=5, enrich=True, reactions={"counts": True, "recent": True},
)
// Get activities from 5 to 10
$results = $userFeed1->getActivities(5, 5);

# Get the 5 activities added after last_activity 
$options = ['id_lt' => $last_activity_id];
$results = $userFeed1->getActivities(0, 5, $options);

# Get the 5 activities added before last_activity 
$options = ['id_gt' => $last_activity_id];
$results = $userFeed1->getActivities(0, 5, $options);

// Get 5 activities - using limit, offset - and sorted by rank (Ranked Feeds Enabled):
$options = ['ranking' => 'popularity'];
$results = $userFeed1->getActivities(0, 5, $options);
// Get 5 activities with id less than the given UUID (Faster - Recommended!)
response = userFeed.getActivities(new Filter().idLessThan("e561de8f-00f1-11e4-b400-0cc47a024be0").limit(5)).join();
// Get activities from 5 to 10 (Pagination-based - Slower)
response = userFeed.getActivities(new Pagination().offset(0).limit(5)).join();
// Get activities sorted by rank (Ranked Feeds Enabled):
response = userFeed.getActivities(new Pagination().limit(5), "popularity").join();
// Get activities from 5 to 10
resp, err = userFeed.GetActivities(
    stream.WithActivitiesLimit(5),
    stream.WithActivitiesOffset(5),
)
if err != nil {
    panic(err)
}

// Get the 5 activities added after lastActivity 
resp, err := userFeed.GetActivities(
    stream.WithActivitiesLimit(5),
    stream.WithActivitiesIDLT(lastActivity.ID),
)
if err != nil {
    panic(err)
}

// Get activities sorted by rank (Ranked Feeds Enabled):
resp, err = userFeed.GetActivitiesWithRanking(
    "popularity",
    stream.WithActivitiesLimit(5),
)
if err != nil {
    panic(err)
}
// Get activities from 5 to 10
var result = await userFeed1.GetActivities(5, 5);

# Get the 5 activities added after lastActivity 
var result = await userFeed1.GetActivities(0, 5, FeedFilter.Where().IdLessThan(lastActivity.id));

// Get activities sorted by rank (Ranked Feeds Enabled):
var flatActivities = await userFeed1.GetFlatActivities(GetOptions.Default.WithLimit(5).WithRanking("popularity"));

Parameters

Name Type Description Default Optional
limit int The number of Activities to retrieve (max: 100) 25
id_gte string Filter the feed on ids greater than or equal to the given value
id_gt string Filter the feed on ids greater than the given value
id_lte string Filter the feed on ids smaller than or equal to the given value
id_lt string Filter the feed on ids smaller than the given value
offset int The offset 0
ranking string The custom ranking formula used to sort the feed, must be defined in the dashboard
enrich boolean When using collections, you can request Stream to enrich activities to include them false
reactions.own boolean Include reactions added by current user to all activities (see reaction docs) false
reactions.recent boolean Include recent reactions to activities (see reaction docs) false
reactions.counts boolean Include reaction counts to activities (see reaction docs) false

Go flags usage

The flags described above are passed to functions as arguments as variadic functional options. Be sure to check our Go client repository (link) to check the syntax out.

Activity reads returns at most 100 Activities. Requests with a limit greater than 100 are automatically capped.

Pagination

The recommended way to paginate feeds is with offset and limit parameters. Such approach makes for simpler code and it works for all types of feeds.

Note on Aggregated Feeds: When using id_lte to paginate an aggregated feed, use the ID of the group that is returned from the API. Using an ID of an individual activity within the group will not work and result in an error.

Note on Custom Ranking: Sorting using a custom ranking formula is only available on paid plans.

Retrieve Feed response data

When a feed is successfully retrieved, the Stream API returns the following payload:

{
  results: [],
  next: '/api/v1.0/feed/<< feed_group >>/<< feed_id >>/?api_key=<< api_key >>&amp;id_lt=<< next_activity_id >>&amp;limit=<< activity_limit >>'
}
{
  "results": [],
  "next": "/api/v1.0/feed/<< feed_group >>/<< feed_id >>/?api_key=<< api_key >>&amp;id_lt=<< next_activity_id >>&amp;limit=<< activity_limit >>"
}

The format for results array depends on the type of feed associated with the notification. When a Flat Feed is retrieved, the array contains Activities. Whereas when an Aggregated or Notification Feed is retrieved, the array contains Activity Groups.

The 'next' property in the response contains a URL that may be used to retrieve the next page of activities within the feed.

Removing Activities

There are two ways to remove an activity:

  • Activity id - found in the serialized response from server
  • Foreign id - optionally specified when adding an activity
Have a look at the section on Using Foreign IDs.

# Remove an activity by its id
user_feed_1.remove_activity('e561de8f-00f1-11e4-b400-0cc47a024be0')

# Remove activities with foreign_id 'run:1'
user_feed_1.remove_activity('run:1', foreign_id=true)
// Remove an activity by its id
user1.removeActivity("e561de8f-00f1-11e4-b400-0cc47a024be0");
// Remove activities foreign_id 'run:1'
user1.removeActivity({foreignId: 'run:1'});
# Remove an activity by its id
response = user_feed_1.remove_activity("e561de8f-00f1-11e4-b400-0cc47a024be0")

removed_activity_id = response["removed"]

# Remove activities with foreign_id 'run:1'
response = user_feed_1.remove_activity(foreign_id='run:1')
removed_foreign_id = response["removed"]
// Remove an activity by its id
$userFeed1->removeActivity('e561de8f-00f1-11e4-b400-0cc47a024be0');

// Remove activities with foreign_id 'run:1'
$userFeed1->removeActivity('run:1', true)
// Remove an activity by its id
userFeed.removeActivityByID("e561de8f-00f1-11e4-b400-0cc47a024be0").join();

// Remove activities with foreign_id 'run:1'
userFeed.removeActivityByForeignID("run:1").join();
// Remove an activity by its id
err := userFeed.RemoveActivityByID("e561de8f-00f1-11e4-b400-0cc47a024be0")
if err != nil {
    panic(err)
}
// Remove activities with foreign_id 'run:1'
err = userFeed.RemoveActivityByForeignID("run:1")
if err != nil {
    panic(err)
}
// Remove an activity by its id
feed.RemoveActivity("e561de8f-00f1-11e4-b400-0cc47a024be0");

// Remove activities by their foreign_id
feed.RemoveActivity("run:1", true);
Note: When you remove by foreign_id field, all activities in the feed with the provided foreign_id will be removed.

Updating Activities

Activities that have both foreign_id and time defined can be updated via the APIs. Changes to activities immediately show up on every feed.

activity = {
  :actor => "1",
  :verb => "like",
  :object => "3",
  :time => DateTime.now,
  :foreign_id => "like:3",
  :popularity => 100
}

# first time the activity is added
user_feed_1.add_activity(activity)

# update the popularity value for the activity
activity[:popularity] = 10

# send the update to the APIs
client.update_activities([activity])
var now = new Date();

activity = {
    "actor": "1",
    "verb":"like",
    "object": "3",
    "time": now.toISOString(),
    "foreign_id": "like:3",
    "popularity": 100
};

// first time the activity is added
user1.addActivity(activity);

// update the popularity value for the activity
activity.popularity = 10;

// send the update to the APIs
client.updateActivities([activity]);
activity = {
    "actor": "1",
    "verb":"like",
    "object": "3",
    "time": datetime.datetime.utcnow(),
    "foreign_id": "like:3",
    "popularity": 100
}

# first time the activity is added
user_feed_1.add_activity(activity)

# update the popularity value for the activity
activity['popularity'] = 10

# send the update to the APIs
client.update_activities([activity])
$now = new DateTime('now');
$activities = [];

$activity = [
    'actor' => 1,
    'verb' => 'tweet',
    'object' => 1,
    'time' => $now->format(DateTime::ISO8601),
    'foreign_id' => 'batch1',
    'popularity' => 100,
];

// add the activity to a feed
$userFeed1->addActivity(activity);

// change a field of the activity data
$activity['popularity'] = 10;

$activities = [$activity];

// update the activity
$client->updateActivities($activities);
Activity activity = Activity.builder()
        .actor("1")
        .verb("like")
        .object("3")
        .time(new Date())
        .foreignID("like:3")
        .extraField("popularity", 100)
        .build();

// first time the activity is added
userFeed.addActivity(activity).join();

// update the popularity value for the activity
activity = Activity.builder()
        .fromActivity(activity)
        .extraField("popularity", 10)
        .build();

client.batch().updateActivities(activity);
activity := stream.Activity{
    Actor:     "1",
    Verb:      "like",
    Object:    "3",
    Time:      stream.Time{time.Now()},
    ForeignID: "like:3",
    Extra: map[string]interface{}{
        "popularity": 100,
    },
}

// first time the activity is added
_, err := userFeed.AddActivity(activity)
if err != nil {
    panic(err)
}

// update the popularity value for the activity
activity.Extra["popularity"] = 10

// send the update to the APIs
err = userFeed.UpdateActivities(activity)
if err != nil {
    panic(err)
}
var activity = new Activity("1", "like", "3")
{
    Time = DateTime.Now,
    ForeignId = "like:3"
};
activity.SetData("popularity", 100);

// first time the activity is added
await userFeed1.AddActivity(activity);

// update the popularity value for the activity
activity.SetData("popularity", 10);

// send the update to the APIs
await client.Batch.UpdateActivity(activity);
Note: When you update an activity, you must include the following fields both when adding and updating the activity:
  • time
  • foreign_id
Note: It is not possible to update more than 100 activities per request with this method.
Note: When updating an activity any changes to the to field are ignored.

This API method works particularly well in combination with the ranked feeds. You can, for instance, issue an update if an activity is promoted or not and use the ranked feeds to show it higher in the feed. Similarly, you could update the like and comment counts and use a ranking method based on popularity to sort the activities.

Activity partial update

It is possible to update only a part of an activity with the partial update request. You can think of it as a quick "patching operation".

The activity to update can be selected by its ID or Foreign ID and Time combination.

A set and an unset params can be provided to add, modify, or remove attributes to/from the target activity. The set and unset params can be used separately or combined together (see below).

# partial update by activity ID
client.activity_partial_update(
  id: '54a60c1e-4ee3-494b-a1e3-50c06acb5ed4',
  set: {
    'product.price': 19.99,
    'shares': {
      'facebook': '...',
      'twitter': '...'
    },
  },
  unset: [
    'daily_likes',
    'popularity'
  ]
)

# partial update by foreign ID
client.activity_partial_update(
  foreign_id: 'product:123',
  time: '2016-11-10T13:20:00.000000',
  set: {
    ...
  },
  unset: [
    ...
  ]
)
// partial update by activity ID
client.activityPartialUpdate({
  id: '54a60c1e-4ee3-494b-a1e3-50c06acb5ed4',
  set: {
    'product.price': 19.99,
    'shares': {
      'facebook': '...',
      'twitter': '...'
    },
  },
  unset: [
    'daily_likes',
    'popularity'
  ]
})

// partial update by foreign ID
client.activityPartialUpdate({
  foreignID: 'product:123',
  time: '2016-11-10T13:20:00.000000',
  set: {
    ...
  },
  unset: [
    ...
  ]
})
# partial update by activity ID
client.activity_partial_update(
  id='54a60c1e-4ee3-494b-a1e3-50c06acb5ed4',
  set={
    'product.price': 19.99,
    'shares': {
      'facebook': '...',
      'twitter': '...'
    },
  },
  unset=[
    'daily_likes',
    'popularity'
  ]
)

# partial update by foreign ID
client.activity_partial_update(
  foreign_id='product:123',
  time='2016-11-10T13:20:00.000000',
  set={ ... },
  unset=[ ... ],
)
# Not supported yet
// prepare the set operations
Map<String, Object> set = new ImmutableMap.Builder<String, Object>()
        .put("product.price", 19.99)
        .put("shares", new ImmutableMap.Builder<String, Object>()
                .put("facebook", "...")
                .put("twitter", "...")
                .build())
        .build();
// prepare the unset operations
String[] unset = new String[] { "daily_likes", "popularity" };

// partial update by activity ID
String id = "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4";
client.updateActivityByID(id, set, unset).join();

// partial update by foreign ID
String foreignID = "product:123";
Date timestamp = new Date();
client.updateActivityByForeignID(foreignID, timestamp, set, unset).join();
// partial update by activity ID

// prepare the set operations
set := map[string]interface{}{
    "product.price": 19.99,
    "shares": map[string]interface{}{
        "facebook": "...",
        "twitter": "...",
    },
}
// prepare the unset operations
unset := []string{"daily_likes", "popularity"}

id := "54a60c1e-4ee3-494b-a1e3-50c06acb5ed4"
resp, err := client.UpdateActivityByID(id, set, unset)
if err != nil {
    panic(err)
}

// partial update by foreign ID
foreignID := "product:123"
timestamp := stream.Time{/*...*/}
resp, err := client.UpdateActivityByForeignID(foreignID, timestamp, set, unset)
if err != nil {
    panic(err)
}
// partial update by activity ID
var set = new GenericData();
set.SetData("product.price", 19.99);
set.SetData("product.price", new Dictionary<string, string>()
    {
         {"facebook", "..."},
         {"twitter", "..."}
    });
var unset = new string[]{"daily_likes", "popularity"};
await client.ActivityPartialUpdate("54a60c1e-4ee3-494b-a1e3-50c06acb5ed4", null, set, unset);

// partial update by foreign ID
var foreignIDTime = new ForeignIDTime("product:123", DateTime.Parse("2016-11-10T13:20:00.000000"));
await client.ActivityPartialUpdate(null, foreignIDTime, set, unset);

Parameters

Name Type Description Optional
id string The target activity ID
foreign_id string The target activity foreign ID
time string The target activity timestamp
set object The set operations, max 25 top level keys.
unset list The unset operations, max 25.

The set object contains the insertion updates for the target fields, where the keys are the activity fields to update and the values are the new ones to assign. If the target field does not exist, it's created with the given content. If the target field exists, its content is replaced.

It is possible to quickly reference nested elements using the dotted notation (father.child. ...), but in this case the whole hierarchy is required to exist and to be valid.

For example, if the target activity looks like the following:

{
    "id": "...", "actor": "...", "object": "...", "verb": "...", "foreign_id": "...",
    "product": {
        "sku": 12345,
        "price": {
            "eur": 7.99,
            "usd": 9.99
        },
        "name": "shoes",
        "popularity": 9000
    }
}

it is possible to update the product's EUR price with the key "product.price.eur", or even create a new field with the key "product.price.gbp", but it's not possible to reference a non-existing hierarchy like "product.colors.blue".

The unset field contains a list of key strings that will be removed from the activity's payload. They must exist, and, if referenced with the dotted notation, their hierarchy must be valid.

The set and unset fields can be combined in the same request, but they must not be conflicting with each other (they must not have operations referencing the same keys or hierarchies).

The following example shows a valid combination of set and unset operations which will update the above example activity:

{
    "set": {
        "product.price.eur": 8.99,
        "product.price.gbp": 6.99,
        "product.available": {
            "blue": 100,
            "black": 300
        },
        "facebook_page": "..."
    },
    "unset": [
        "product.price.usd",
        "popularity"
    ]
}
Note: it is not possible to include the following reserved fields in a partial update request (set or unset):
  • id
  • actor
  • verb
  • object
  • time
  • target
  • foreign_id
  • to
  • origin
Note: the size of an activity's payload must be less than 10KB after all set and unset operations are applied. The size is based on the size of the JSON-encoded activity.

Uniqueness & Foreign ID

Stream handles uniqueness based on the foreign_id and time fields. If you want to be able to update your activities you need to specify both of these fields.


Uniqueness is enforced on the combination of time & foreign_id. See the example below for an overview:

first_activity = {
  :actor => "1",
  :verb => "like",
  :object => "4",
  :time => DateTime.now,
  :foreign_id => "like:3"
}
first_activity_id = user_feed_1.add_activity(first_activity)['id']

second_activity = {
  :actor => "1",
  :verb => "like",
  :object => "4",
  :time => DateTime.now,
  :foreign_id => "like:3"
}
second_activity_id = user_feed_1.add_activity(second_activity)['id']

# The unique combination of foreign_id and time ensure that both
# activities are unique and therefore the first_activity_id != second_activity_id
var first_activity = {
  actor: 1,
  verb: 'add',
  object: '1',
  foreign_id: 'activity_1'
  time: new Date().toISOString()
};

// Add activity to activity feed:
var first_activity_id;
user_feed_1.addActivity(first_activity)
    .then(function(activity_response) {first_activity_id = activity_response.id;})
    .catch(function(error) { /* Handle error */ });

var second_activity = {
  actor: 1,
  verb: 'add',
  object: '1',
  foreign_id: 'activity_2'
  time: new Date().toISOString()
};

var second_activity_id;
user_feed_1.addActivity(second_activity)
    .then(function(activity_response) {second_activity_id = activity_response.id;})
    .catch(function(error) { /* Handle error */ });


/*
The unique combination of foreign_id and time ensure that both
activities are unique and therefore the first_activity_id != second_activity_id
*/
from datetime import datetime

first_activity = {
    "actor": "1",
    "verb":"like",
    "object": "3",
    "time":datetime.utcnow(),
    "foreign_id": "like:3"
}
first_activity_id = user_feed_1.add_activity(first_activity)['id']

second_activity = {
    "actor": "1",
    "verb":"like",
    "object": "3",
    "time": datetime.utcnow(),
    "extra": "extra_value",
    "foreign_id": "like:3"
}
second_activity_id = user_feed_1.add_activity(second_activity)['id']

# The unique combination of foreign_id and time ensure that both
# activities are unique and therefore the first_activity_id != second_activity_id
Activity firstActivity = userFeed.addActivity(Activity.builder()
        .actor("1")
        .verb("like")
        .object("3")
        .time(new Date())
        .foreignID("like:3")
        .build()).join();
Activity secondActivity = userFeed.addActivity(Activity.builder()
        .actor("1")
        .verb("like")
        .object("3")
        .time(new Date())
        .extraField("extra", "extra_value")
        .foreignID("like:3")
        .build()).join();
// The unique combination of foreign_id and time ensure that both
// activities are unique and therefore the firstActivity ID != secondActivity ID
now := time.Now()
firstActivity, err := userFeed.AddActivity(stream.Activity{
    Actor:     "1",
    Verb:      "like",
    Object:    "3",
    Time:      stream.Time{now},
    ForeignID: "like:3",
})
if err != nil {
    panic(err)
}
secondActivity, err := userFeed.AddActivity(stream.Activity{
    Actor:  "1",
    Verb:   "like",
    Object: "3",
    Time:   stream.Time{now},
    Extra: map[string]interface{}{
        "extra": "extra_value",
    },
    ForeignID: "like:3",
})
if err != nil {
    panic(err)
}
// foreign ID and time are the same for both activities
// hence only one activity is created and first and second IDs are equal
// firstActivity.ID == secondActivity.ID
var firstActivity = new Stream.Activity("1", "like", "3")
{
    Time = DateTime.UtcNow,
    ForeignId = "like:3"
};

var firstActivityID = (await userFeed1.AddActivity(firstActivity)).Id;

var secondActivity = new Stream.Activity("1", "like", "3")
{
    Time = DateTime.UtcNow,
    ForeignId = "like:3"
};
secondActivity.SetData("extra", "extra_value");

var secondActivityID = (await userFeed1.AddActivity(secondActivity)).Id;

// The unique combination of foreign_id and time ensure that both
// activities are unique and therefore the first_activity_id != second_activity_id

Following Feeds

Following relationships are a fundamental part of social networks and many other apps that feature feeds. They link one Feed to another and cause Activities added to a feed to appear in other any other feeds that follow it.

When an Activity is added to a feed, it is automatically added to any other feeds that follow the feed. This does not propagate further through the graph of following relationships. If your app requires that the Activity be added to other feeds, "to" field targetting or a batch activity add may be suitable.

The code example below shows you how to follow a feed:

# timeline:timeline_feed_1 follows user:user_42
timeline_feed_1 = client.feed('timeline', 'timeline_feed_1')
timeline_feed_1.follow('user', 'user_42')

# follow feed without copying the activities:
timeline_feed_1.follow('user', 'user_42', activity_copy_limit=0)
// timeline:timeline_feed_1 follows user:user_42:
timeline_feed_1.follow('user', 'user_42');

// Follow feed without copying the activities:
timeline_feed_1.follow('user', 'user_42', { limit: 0 });
# timeline:timeline_feed_1 follows user:user_42
timeline_feed_1.follow("user", "user_42")

# Follow feed without copying the activities
timeline_feed_1.follow("user", "user_42", activity_copy_limit=0)
// timeline:timeline_feed_1 follows user:user_42
$timeline1 = $client->feed('timeline', 'timeline_1');
$timeline1->follow('user', 'user_42');

// follow feed without copying the activities:
$timeline1->follow('user', 'user_42', 0)
// timeline:timeline_feed_1 follows user:user_42
FlatFeed user = client.flatFeed("user", "user_42");
FlatFeed timeline = client.flatFeed("timeline", "timeline_feed_1");
timeline.follow(user);

// follow feed without copying the activities:
timeline.follow(user, 0);
// timeline:timeline_feed_1 follows user:user_42
user := client.FlatFeed("user", "user_42")
timeline := client.FlatFeed("timeline", "timeline_feed_1")
err := timeline.Follow(user)
if err != nil {
    panic(err)
}

// follow feed without copying the activities:
timeline.Follow(user, stream.WithFollowFeedActivityCopyLimit(20))
// timeline:timeline_feed_1 follows user:user_42
var timelineFeed1 = client.Feed("timeline", "timeline_feed_1");
await timelineFeed1.FollowFeed("user", "user_42");

// follow feed without copying the activities:
await timelineFeed1.FollowFeed("user", "user_42", 0);

Also take note that:

  • Only Flat Feeds may be followed
  • A Feed cannot follow itelf

By default, the most recent 300 existing activities in the target feed will be added to the follower feed (asynchronously), but this can be changed via the activity_copy_limit parameter.

Parameters

Name Type Description Default Optional
feed string The feed id
target string The feed id of the target feed
activity_copy_limit integer How many activities should be copied from the target feed, max 1000 300

If you need to follow many feeds at once have a look at how to batch follow later in this document. If you need to run large imports, use our JSON import format.

Unfollowing Feeds

Removes the following relationship with the feed specified in the target parameter. An example below:

# Stop following feed user_42
timeline_feed_1.unfollow('user', 'user_42')

# Stop following feed 42 but keep history of activities
timeline_feed_1.unfollow('user', 'user_42', keep_history=true)
// Stop following feed user_42 - purging history:
timeline_feed_1.unfollow('user', 'user_42');

// Stop following feed user_42 but keep history of activities:
timeline_feed_1.unfollow('user', 'user_42', { keepHistory: true });
# Stop following feed user_42
timeline_feed_1.unfollow("user", "user_42")

# Stop following feed user_42 but keep history of activities
timeline_feed_1.unfollow("user", "user_42", keep_history=True)
// Stop following feed user_42
$timelineFeed1->unfollow('user', 'user_42');

// Stop following feed user_42 but keep history of activities:
$timelineFeed1->unfollow('user', 'user_42', true);
// Stop following feed user:user_42
timeline.unfollow(user);

// Stop following feed user:user_42 but keep history of activities
timeline.unfollow(user, KeepHistory.YES);
// user := client.FlatFeed("user", "42")

// Stop following feed user:user_42
err := timeline.Unfollow(user)
if err != nil {
    panic(err)
}
// Stop following feed user:user_42 but keep history of activities
err = timeline.Unfollow(user, stream.WithUnfollowKeepHistory(true))
if err != nil {
    panic(err)
}
var timelineFeed1 = client.Feed("timeline", "timeline_feed_1");
// Stop following feed user_42
await timelineFeed1.UnfollowFeed("user", "user_42");

// Stop following feed 42 but keep history of activities
await timelineFeed1.UnfollowFeed("user", "user_42", true);

Existing Activities in the feed that originated in the target, no longer followed, feed will be purged (asynchronously) unless the keep_history parameter is provided.

Since Go does not provide optional parameters, you will need to use the UnfollowKeepingHistory() method.

Parameters

Name Type Description Default Optional
feed string The feed id
target string The feed id of the target feed
keep_history boolean Whether the activities from the unfollowed feed should be removed False
Note:Re-following a feed that is already followed has no effect. However unfollowing and re-following a feed will result in feed updates as Activities are purged and re-added to the feed.

Reading Feed Followers

Returns a paginated list of the given feed's followers. Code example below:

# list followers
user_feed_1.followers(0, 10)
// List followers
user1.followers({limit: '10', offset: '10'});
# list followers
user_feed_1.followers(offset=0, limit=10)
// list followers
$userFeed1->followers(0, 10);
// list followers
List<FollowRelation> followers = userFeed.getFollowers(new Pagination().offset(0).limit(10)).get();
for (FollowRelation follow : followers) {
    System.out.format("%s -> %s", follow.getSource(), follow.getTarget());
    // ...
 }
// list followers
resp, err := userFeed.GetFollowers(stream.WithFollowersOffset(0), stream.WithFollowersLimit(10))
for _, follower := range resp.Results {
    fmt.Println(follower.FeedID, "->", follower.TargetID)
    // ...
}
await userFeed1.Followers(0, 10);

The followers returned by the API are sorted reverse chronologically, according to the time the follow relationship was created.

Note: the number of followers that can be retrieved is limited to 1000.

Parameters

Name Type Description Default Optional
limit int Amount of results per request, max 100 25
offset int Number of rows to skip before returning results, max 999 0

Response data

{results: [
    {
      feed_id: 'conversation2:test',
      target_id: 'conversation2:user_2',
      created_at: '2018-01-19T09:35:17.817332Z'
    }
  ],
  duration: "1.92ms"
}
{
  "results"': [
    {
      "feed_id": "conversation2:test",
      "target_id": "conversation2:user_2",
      "created_at": "2018-01-19T09:35:17.817332Z"
    }
  ],
  "duration": "1.92ms"
}

Reading Followed Feeds

Returns a paginated list of the feeds which are followed by the feed. Code example below:

# Retrieve 10 feeds followed by user_feed_1
user_feed_1.following(10)

# Retrieve 10 feeds followed by user_feed_1 starting from the 11th
user_feed_1.following(10, 10)

# Check if user_feed_1 follows specific feeds
user_feed_1.following(0, 2, filter=['user:42', 'user:43'])
// Retrieve last 10 feeds followed by user_feed_1
user1.following({offset: 0, limit: 10});

// Retrieve 10 feeds followed by user_feed_1 starting from the 11th
user1.following({offset: 10, limit: 10});

// Check if user1 follows specific feeds
user1.following({offset: 0, limit: 2, filter: ['user:42', 'user:43']})
# Retrieve last 10 feeds followed by user_feed_1
user_feed_1.following(offset=0, limit=10)

# Retrieve 10 feeds followed by user_feed_1 starting from the 11th
user_feed_1.following(offset=10, limit=10)

# Check if user_feed_1 follows specific feeds
user_feed_1.following(offset=0, limit=2, feeds=['user:42', 'user:43'])
// Retrieve 10 feeds followed by $userFeed1
$userFeed1->following(0, 10);

// Retrieve 10 feeds followed by $userFeed1 starting from the 10th (2nd page)
$userFeed1->following(10, 10);

// Check if $userFeed1 follows specific feeds
$userFeed1->following(0, 2, ['user:42', 'user:43']);
// Retrieve last 10 feeds followed by user_feed_1
List<FollowRelation> followed = userFeed.getFollowed(new Pagination().offset(0).limit(10)).get();

// Retrieve 10 feeds followed by user_feed_1 starting from the 11th
followed = userFeed.getFollowed(new Pagination().offset(10).limit(10)).get();

// Check if user_feed_1 follows specific feeds
followed = userFeed.getFollowed(new Pagination().offset(0).limit(2), new FeedID("user:42"), new FeedID("user", "43")).get();
// Retrieve last 10 feeds followed by user_feed_1
resp, err := userFeed.GetFollowing(stream.WithFollowingOffset(0), stream.WithFollowingLimit(10))
if err != nil {
    panic(err)
}

// Retrieve 10 feeds followed by user_feed_1 starting from the 11th
resp, err = userFeed.GetFollowing(stream.WithFollowingOffset(10), stream.WithFollowingLimit(10))
if err != nil {
    panic(err)
}

// Check if user_feed_1 follows specific feeds
resp, err = userFeed.GetFollowing(
    stream.WithFollowingOffset(0),
    stream.WithFollowingLimit(2),
    stream.WithFollowingFilter("user:42", "user:43"),
)
if err != nil {
    panic(err)
}
// Retrieve last 10 feeds followed by userFeed1
await userFeed1.Following(0, 10);

// Retrieve 10 feeds followed by userFeed1 starting from the 11th
await userFeed1.Following(10,  10);

// Check if user1 follows specific feeds
await userFeed1.Following(0, 2, new string[] { "user:42", "user:43" });

The followed feeds returned by the API are sorted reverse chronologically, according to the time the follow relationship was created.

Note: the number of followed feeds that can be retrieved is limited to 1000.

Parameters

Name Type Description Default Optional
limit int Amount of results per request, max 100 25
offset int Number of rows to skip before returning results, max 999 0
filter string The comma separated list of feeds to filter results on

Our Go client library does not support filtering in FollowingWithLimitAndSkip()

Response data

{results: [
     {
       feed_id: 'conversation2:test',
       target_id: 'conversation2:user_2',
       created_at: '2018-01-19T09:35:17.817332Z',
       updated_at: null
     }
   ],
  duration: '1.92ms'
}
{'results': [
     {
       'feed_id': 'conversation2:test',
       'target_id': 'conversation2:user_2',
       'created_at': '2018-01-19T09:35:17.817332Z',
       'updated_at': None
     }
   ],
  'duration': '1.92ms'
}

Creating Feed Groups

Feed Types

There are 3 feed types:

Based on these 3 basic feed types you can create your own feed groups.

"Which Feed Groups Should I Create?"

We recommend that you create different feed groups for adding activities, and for consuming activities.

Flat are the only feeds that can be followed, and therefore are a good type to setup for adding activities. Flat can also be used to consume activities from other feeds - in a "timeline"-like manner.

Aggregated are good for consuming activities in an "aggregated"-like manner. You cannot follow an aggregated feed, but you may on occasion want to add activities to one.

The Notification type makes it easy to add notifications to your app. Notifications cannot be followed by other feeds - but you can write directly to a Notification feed.

Take for example that you are building a music app like Spotify.

You might set up the following feeds:

  • "notification" (Notification): Displaying notifications from anything a user is following.
  • "user" (Flat): Adding activities from a user that can be followed.
  • "artist" (Flat): Adding activities from an artist that can be followed.
  • "playlist" (Flat): Adding activities from a playlist that can be followed.
  • "timeline" (Flat): Displaying "user", "artist", or "playlist" activities.

"How Do I Create Feeds?"

New feeds can be created in the Feeds section of the Dashboard.

By default, we setup four feed groups for your project:

These four groups cover the average use-cases for most applications.

Flat Feeds

Flat is the default feed type - and the only feed type that you can follow. It's not possible to follow either aggregated or notification feeds.

Create new feed groups based on the flat type in the dashboard.

Options

There are several options you can configure:

Name Description Default
Realtime notifications Enable realtime notifications True

Aggregated feeds

Aggregated feeds are helpful if you want to group activities. Here are some examples of what you can achieve using aggregated feeds:

  • Eric followed 10 people
  • Julie and 14 others like your photo

You can create new aggregated feed groups in the dashboard.

Options

There are several options you can configure for aggregated feeds. The most important setting is the aggregation format. This format determines how activities are grouped together.

Name Description Default
Realtime notifications Enable realtime notifications True
Aggregation Format The aggregation format to use. This is used as a key to group activities by. {{ verb }}_{{ time.strftime("%Y-%m-%d") }}

How aggregated feeds work

When you insert an activity to an aggregated feed we apply the aggregation format. The aggregation format + the activity's data lead to a value for the "group". This group is used to combine the activities.

The aggregation format is applied at write time. Changing the aggregation format only affects activities inserted after the change. The dashboard allows you to preview changes on your existing data though.

Aggregation Format Syntax

The following variables are available:

  • verb the activity verb as it is send to the APIs (e.g. "like")
  • time the activity creation date and time (or the value sent to APIs)
  • object the activity object as it is sent to the APIs (e.g. Item:13)
  • actor the activity actor (e.g. User:9)
  • feed.group the feed group name (e.g. if the feed is "notification:123", feed.group is notification)
  • feed.user_id the feed user id (e.g. if the feed is "notification:123", feed.user_id is 123))
  • id the internal unique activity id. This will be unique so if you use this nothing will be aggregated. Use this when you don't want aggregation, but only the mark_read/mark_seen functionality.

In addition, you can use any of the custom values you've provided when adding the activity.

Here are some common examples of aggregation formats:

  • Per actor, verb id and day:
    {{ actor }}_{{ verb }}_{{ time.strftime("%Y-%m-%d") }}
  • Per activity verb and day:
    {{ verb }}_{{ time.strftime("%Y-%m-%d") }}
  • Follows by actor, everything else grouped by verb and day
    {% if verb == 'follow' %} {{ actor }}_{{ verb }}_{{ time.strftime("%Y-%m-%d") }} {% else %} {{ verb }}_{{ time.strftime("%Y-%m-%d") }} {% endif %}

The aggregation rule uses the Jinja2 Template Syntax. Control structures like if and else are avaliable, as well as these filters: int, lower and strftime to format the time variable. The options for strftime are documented here. Please get in touch with support if you need help with your aggregation formula.

Note: Each activity group inside an aggregated feed contains the 15 most recent activities. If you add more than 15 activities to one group, older activities will no longer be visible. Enterprise customers can contact support to increase this limit.

Data format

The data format in which Aggregated Feeds are returned differs from regular Flat Feeds.

At the top level of the JSON response the 'results' field contains an array of Activity Groups plus additional metadata about the feed. Within the results array, each Activity Group contains a count of Activities and unique 'actors' contained within the group.

Notification Feeds

Notification Feed Groups extend the Aggregated Feed Group concept with additional features that make them well suited to notification systems:

  • Notification Feeds contain Activity Groups, each with a "seen" and "read" status field. These fields can be updated to reflect how a user has interacted with a given notification.
  • When retrieved, the Feed includes a real-time count of the total number of unseen and unread Activity Groups (notifications).

Take for example the notification system on Facebook. If you click the notification icon, all notifications get marked as seen. However, an individual notification only gets marked as read when you click on it.

You can create new Notification Feed Groups in the dashboard.

Options

There are several options to configure:

Name Description Default
Realtime notifications Enable realtime notifications True
Aggregation Format The aggregation format to use. The default {{ verb.id }}_{{ time.strftime("%Y-%m-%d") }} (verbs grouped by day) or {{ id }} (to achieve a single activity per group) are most commonly used. {{ verb }}_{{ time.strftime("%Y-%m-%d") }}

Data format

Similar to Aggregated Feeds, the data format in which Notification Feeds are retrieved differs from regular Flat Feeds.

At the top level of the JSON response the 'results' field contains an array of Activity Groups, along with overall 'unseen' and 'unread' counts and other metadata about the feed. Within the results array, each Activity Group contains the additional status fields that indicate the group is seen/read.


          The example "notification_feed_retrieve_example_data" couldn't be loaded
 	  		

When reading the feed, set the mark_read and mark_seen parameter. The next time the feed is read, the seen and/or read status will be updated accordingly.

Name Description Optional
mark_read Comma separated list of activity group ids to mark as read, or True which means all
mark_seen Comma separated list of activity group ids to mark as seen, or True which means all

If passing IDs to mark_seen or mark_read, be sure to pass the ID of the activity group as returned by the API, not the IDs of individual activities.

Here's the example using the various clients:

# Mark all activities in the feed as seen
result = notification_feed.get(:limit=>5, :offset=>0, :mark_seen=>true)

# Mark some activities as read via specific Activity Group Ids
result = notification_feed.get(:limit=>5, :mark_read=>['activity_group_id_1', 'activity_group_id_2'])
// Mark all activities in the feed as seen
notificationFeed.get({mark_seen:true})
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });

// Mark some activities as read via specific Activity Group Ids
notificationFeed.get({mark_read: ['activityGroupIdOne', 'activityGroupIdTwo']})
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });
# Mark all activities in the feed as seen
results = notification_feed.get(mark_seen=True)['results']

# Mark some activities as read via specific Activity Group Ids
notification_feed.get(mark_read=[results[0]['id'], results[1]['id']])
# Mark all activities in the feed as seen
$options = ['mark_seen' => true];
$results = $notificationFeed->getActivities(0, 10, $options);

# Mark some activities as read via specific Activity Group Ids
$options = ['mark_read' => ['activity_group_id_1', 'activity_group_id_2']];
$results = $notificationFeed->getActivities(0, 10, $options);
NotificationFeed notifications = client.notificationFeed("notifications", "1");
// Mark all activities in the feed as seen
List<NotificationGroup<Activity>> activityGroups = notifications.getActivities(new ActivityMarker().allSeen()).get();
for (NotificationGroup<Activity> group : activityGroups) {
    // ...
}
// Mark some activities as read via specific Activity Group Ids
activityGroups = notifications.getActivities(new ActivityMarker().read("groupID1", "groupID2" /* ... */)).get();
notifications := client.NotificationFeed("notifications", "1")
// Mark all activities in the feed as seen
resp, err := notifications.GetActivities(stream.WithNotificationsMarkSeen(true))
if err != nil {
    panic(err)
}
for _, activity := range resp.Results {
    // ...
}
// Mark some activities as read via specific Activity Group Ids
resp, err = notifications.GetActivities(stream.WithNotificationsMarkRead(
    false,              // don't mark all
    groupID1, groupID2, //...
))
// Mark all activities in the feed as seen
var result = await notificationFeed.GetActivities(0, 5, null, ActivityMarker.Mark().AllSeen());

// Mark some activities as read via specific Activity Group Ids
var result = await notificationFeed.GetActivities(0, 5, null, ActivityMarker.Mark().Read(new string[] { "activity_group_id_1", "activity_group_id_2" }));

You'll often want to listen to feed changes in real-time. This is explained in the section on real-time updates.

Note: Each activity group inside a notification feed contains the 15 most recent activities. If you add more than 15 activities to one group, older activities will no longer be visible. Enterprise customers can contact support to increase this limit.

Firehose

Stream's Firehose allows you to subscribe to all feed updates and have the shipped to your own infrastructure via different transports.
This is useful if you want to push notifications using your own infrastructure, do offline processing or data warehousing.

Firehose with Amazon SQS

Stream can send all the updates on your feed group to an Amazon SQS queue you own. The Firehose makes it easy to extend Stream. It's often used to send push notifications or emails based on feed activity.

The real-time updates are:

  • Published on an SQS queue you own
  • Stored in the same queue
  • Sent in JSON format and Base64 encoded

A list with the following properties for each item in the list:

Name Type Description
feed string Name of the feed this update was published on
deleted list All activities deleted by this update
new list All activities created by this update
published_at date Time of the update in ISO format

Configuration

To configure an SQS queue go the dashboard and select the feed group for which you want to configure the SQS realtime transport. If you enable "Realtime Notifications" in the form you can open the advanced realtime configuration via the "Realtime Configuration" link. Now enable the SQS transport and configure your queues url, api key, and api secret. If you click the "Test SQS" button we will send a test request to your SQS queue and show any errors with the current configuration if found.

SQS permissions

Stream needs the right permissions on your SQS queue to be able to send realtime updates to it. If updates are not showing up in your queue add the following permission policy to the queue:

    
      {
          "Version": "2012-10-17",
          "Statement": [
              {
                  "Sid": "Stmt1459523779000",
                  "Effect": "Allow",
                  "Action": [
                      "sqs:GetQueueUrl",
                      "sqs:SendMessage",
                      "sqs:SendMessageBatch",
                      "sqs:GetQueueAttributes"
                  ],
                  "Resource": [
                      "arn:aws:sqs:region:acc_id:queue_name"
                  ]
              }
          ]
      }
    
  

Here's an example list of messages read from your SQS queue:

    
        [{
            "deleted":[],
            "new":[{"actor":"1","verb":"tweet","object":"1","target":null,"time":"2014-12-15T17:20:37.258","foreign_id":null,"id":"af781804-847e-11e4-8080-80012fb97b9e","tweet":"Hello world"}],
            "published_at":"2014-12-15T17:20:37.263518+00:00",
            "feed":"user:2",
            "app_id": "123"
        }],
        [{
            "deleted":["38f81366-847f-11e4-9c94-0cc47a024be0"],
            "new":[],
            "published_at":"2014-12-15T17:20:37.263518+00:00",
            "feed":"timeline:1",
            "app_id": "123"
        }]
    
  

Firehose with Webhooks

Stream can send all the updates on your feed group to a webhook you manage. The Firehose makes it easy to extend Stream. It's often used to send push notifications or emails based on feed activity.

Real-time updates are:

  • Sent with a POST request
  • Data is serialized to JSON
  • Single request can contain multiple updates (up to 100 activities per push)

You are responsible for the security of the webhook endpoint. We suggest that you use HTTPS in combination with HTTP simple authentication. (e.g. https://username:password@domain.tld/path/to/webhook/)

Configuration and verification

Webhook url's can be configured directly on the dashboard. Navigate to the app overview page and select the feed group for which you want to configure webhooks. Once "Realtime Notifications" are enabled you can open a dropdown by clicking on "Realtime Configuration". The "Test Webhook" button will perform ownership verification (see below) and send a test POST request to the endpoint. Click save if your configuration is complete and new feed updates will be send to the endpoint in realtime.

To ensure that you are the owner of the endpoint you configure, we perform a verification step on saving a new endpoint. There are two verification methods available: verification via DNS and verfication via HTTP. You need to pass one of these two verifications in order to setup the webhook.

Verification via DNS

Configure a TXT record _getstream for the domain of the webhook with value getstream=your_app_api_key. For example if your webhook endpoint is https://example.com/feeds/ you will need to create a TXT record _getstream.example.com with value getstream=your_app_api_key.

Verification via HTTP request

To validate using a GET request, make sure the webhook returns your most recent API Key on a GET request. Make sure that the response body is in plain text (no markup, whitespaces, ...).

Example data

Please find below an example of the data your webhook will receive:

    
      [
        {
          "deleted":[],
          "new":[{"actor":"1","verb":"tweet","object":"1","target":null,"time":"2014-12-15T17:20:37.258","foreign_id":null,"id":"af781804-847e-11e4-8080-80012fb97b9e","tweet":"Hello world"}],
          "published_at":"2014-12-15T17:20:37.263518+00:00",
          "feed":"user:2",
          "app_id": "123"
        },
        {
          "deleted":["38f81366-847f-11e4-9c94-0cc47a024be0"],
          "new":[],
          "published_at":"2014-12-15T17:20:37.263518+00:00",
          "feed":"timeline:1",
          "app_id": "123"
        }
      ]
    
  

Custom Ranking

Stream allows you to configure your own ranking method. You can add multiple ranking methods to a given feed group.

Note: Ranked feeds are only available on paid plans. Contact support after upgrading your account to enable ranked feeds for your organization.

An example ranking config is shown below:

{
    "score": "decay_linear(time) * popularity ^ 0.5",
     "defaults": {
        "popularity": 1
    }
}

Available Functions

Name Description
Basic Arithmetic The following are supported: + - * / ( ) ^
ln(x) Log Function natural base
log(x) Log Function base 10
decay_linear(t) Linear Decay
decay_exp(t) Exponential Decay
decay_gauss(t) Gauss Decay
rand_normal() Returns a normally distributed number in the range [-inf, +inf] with standard normal distribution (mean = 0, stddev = 1)
rand_normal(a,b,σ,µ) Returns a normally distributed number in the range [a, b] with standard normal distribution

Adding Activities

Below is an example of how to add activities with custom ranking:

# Instantiate a feed object
user_feed_1 = client.feed('user', '1')

# Add an activity to the feed - where actor, object, and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"):
activity_data = { :actor => "User:2", :verb => "pin", :object => "Place:42", :target => "Board:1", :popularity => 5 }

activity_response = user_feed_1.add_activity(activity_data)
// Server-Side: Instantiate a feed using feed class 'user' and user id '1'
var user1 = client.feed('user', '1');

// Client-Side: Instantiate a feed for feed group 'user', user id '1' and a security token generated server side
var user1 = client.feed('user', '1', $token);

// Add an activity to the feed - adding your ranking method as a parameter (in this case, "popularity"):
var activity = { "actor": "User:2", "verb": "pin", "object": "Place:42", "target": "Board:1", "popularity": 5 };

// Add Activity - Returns promise:
user1.addActivity(activity)
    .then(onSuccess)
    .catch(onFailure);
# Instantiate a feed object
user_feed_1 = client.feed('user', '1')

# Add an activity to the feed, where actor, object and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"):
activity_data = { "actor": "User:2", "verb": "pin", "object": "Place:42", "target": "Board:1", "popularity": 5 }


activity_response = user_feed_1.add_activity(activity_data)
// Instantiate a feed object
$userFeed1 = $client->feed('user', '1');

// Add an activity to the feed, where actor, object and target are references to objects  - and adding your ranking method as a parameter (in this case, "popularity"):
$data = [
    "actor" =>"User:2",
    "verb" =>"pin",
    "object" =>"Place:42",
    "target" =>"Board:1",
    "popularity" => 5,
];

$userFeed1->addActivity($data);
// Add an activity to the feed, where actor, object and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"):
Activity activity = Activity.builder()
        .actor("User:1")
        .verb("pin")
        .object("place:42")
        .target("board:1")
        .extraField("popularity", 5)
        .build();
userFeed.addActivity(activity).join();
// Instantiate a feed object
userFeed := client.FlatFeed("user", "1")

// Add an activity to the feed, where actor, object and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"):
activity := stream.Activity{
    Actor:  "User:1",
    Verb:   "pin",
    Object: "Place:42",
    Target: "Board:1",
    Extra: map[string]interface{}{
        "popularity": 5,
    },
}
_, err := userFeed.AddActivity(activity)
if err != nil {
    panic(err)
}
// Instantiate a feed object
var userFeed1 = client.Feed("user", "1");

// Add an activity to the feed, where actor, object and target are references to objects - adding your ranking method as a parameter (in this case, "popularity"):
var activity = new Activity("User:2", "pin", "Place:42")
{
    Target = "Board:1"
};
activity.SetData("popularity", 5);

var activityResponse = await userFeed1.AddActivity(activity);

Retrieving Activities

Below is an example of how to retrieve activities sorted by custom ranking:

# Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
result = user_feed_1.get(:limit=>5, :ranking=>'activity_popularity')
/*
 * Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
 */
user1.get({ limit:20, ranking:'activity_popularity'})
  .then(activitiesSuccess)
  .catch(activitiesError);
# Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
result = user_feed_1.get(limit=5, ranking="activity_popularity")
// Get 5 activities - using limit, offset - and sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled):
$options = ['ranking' => 'activity_popularity'];

$results = $userFeed1->getActivities(0, 5, $options);
// Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
response = userFeed.getActivities(new Pagination().limit(5), "activity_popularity").join();
// Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
resp, err := userFeed.GetActivitiesWithRanking("activity_popularity", stream.WithActivitiesLimit(5))
if err != nil {
    panic(err)
}
for _, activity := range resp.Results {
    // ...
}
// Get activities sorted by the ranking method labelled 'activity_popularity' (Ranked Feeds Enabled)
var result = await userFeed1.GetFlatActivities(GetOptions.Default.WithLimit(5).WithRanking("activity_popularity"));
Please note: you can only use limit and offset for pagination with custom ranking.

Configuring ranking can be complex. Feel free to reach out to support if you have questions.

Decay & Custom Ranking

Stream supports a Linear, Exponential, and Gauss decay. For each decay function, we support 4 arguments: Origin, Scale, Offset, and Decay. You can pass either a timedelta string (such as 3d, 4w), or a numeric value.

Name Description Default
origin The best possible value. If the value is equal to the origin the decay function will return 1. now
scale Determines how quickly the score drops from 1.0 to the decay value. If the scale is set to "3d" the score will be equal to the decay value after 3 days. 5d
offset Values below the offset will start to receive a lower score. 0
decay The score that a value at scale distance from the origin should receive. 0.5
direction left, right or both. If right is specified only apply the decay for the right part of the graph. both
Note: You can use s, m, h, d and w to specify seconds, minutes, hours, days and weeks respectively.

The example below defines a simple_gauss with the following params:

  • scale: 5 days
  • offset: 1 day
  • decay: 0.3 day

This means that an activity younger than 1 day (the offset) will return a score of 1. An activity which is exactly 6 days old (offset + scale) will get a score of 0.3 (the decay factor). The full JSON config is shown below:

{
  "functions":{
    "simple_gauss":{
      "base":"decay_gauss",
      "scale":"5d",
      "offset":"1d",
      "decay":"0.3"
    },
    "popularity_gauss":{
      "base":"decay_gauss",
      "scale":"100",
      "offset":"5",
      "decay":"0.5"
    }
  },
  "defaults": {
      "popularity": 1
  },
  "score":"simple_gauss(time)*popularity"
}

You can specify defaults for the variables you use. The example above sets popularity to 1 if it's missing from the activity. We highly recommend setting up defaults to prevent errors when retrieving activities without these variables.

Configuring ranking can be complex. Feel free to reach out to support if you have questions!

Targeting Using the "TO" Field

The "TO" field allows you to specify a list of feeds to which the activity should be copied. One way to think about it is as the CC functionality of email.

Use Case: Mentions

Targeting is useful when you want to support @mentions and notify users. An example is shown below:

# Add the activity to Eric's feed and to Jessica's notification feed
data = {
    :actor => "user:Eric",
    :message => "@Jessica check out getstream.io it's totally awesome - totally.",
    :verb => "tweet",
    :object => "tweet:id",
    :to => ["notification:Jessica", ]
}
user_feed_1.add_activity(data);
// Add the activity to Eric's feed and to Jessica's notification feed
var activity = {
    "actor":"user:Eric",
    "message": "@Jessica check out getstream.io it's awesome!",
    "verb":"tweet",
    "object":"tweet:id",
    "to":["notification:Jessica"]
};
user_feed_1.addActivity(activity)
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });
// In production use user ids, not their usernames
# Add the activity to Eric's feed and to Jessica's notification feed
activity = {
    "actor":"user:Eric",
    "message": "@Jessica check out getstream.io it's so dang awesome.",
    "verb":"tweet",
    "object":"tweet:id",
    "to":["notification:Jessica"]
}
user_feed_1.add_activity(activity)
# In production use user ids, not their usernames
// Add the activity to Eric's feed and to Jessica's notification feed
$data = [
    "actor"=>"user:Eric",
    "message"=>"@Jessica check out getstream.io it's awesome!!!1",
    "verb"=>"tweet",
    "object"=>"tweet:id",
    "to"=> ["notification:Jessica"],
];

$userFeed1->addActivity($data);
// Add the activity to Eric's feed and to Jessica's notification feed
activity = Activity.builder()
        .actor("User:Eric")
        .verb("tweet")
        .object("tweet:id")
        .to(Lists.newArrayList(new FeedID("notification:Jessica")))
        .extraField("message", "@Jessica check out getstream.io it's so dang awesome.")
        .build();
userFeed.addActivity(activity);
// Add the activity to Eric's feed and to Jessica's notification feed
activity := stream.Activity{
    Actor:  "user:Eric",
    Verb:   "tweet",
    Object: "tweet:id",
    To: []string{
        "notification:Jessica", // In production use user ids, not their usernames
    },
    Extra: map[string]interface{}{
        "message": "@Jessica check out getstream.io it's so dang awesome.",
    },
}
_, err := userFeed.AddActivity(activity)
if err != nil {
    panic(err)
}
// Add the activity to Eric's feed and to Jessica's notification feed
var activity = new Activity("user:Eric", "tweet", "tweet:id")
{
    To = new string[] { "notification:Jessica" }
};
activity.SetData("message", "@Jessica check out getstream.io it's so dang awesome.");

await userFeed1.AddActivity(activity);
// In production use user ids, not their usernames
Note: The "TO" field is limited to a maximum of 100 targets per API request.

Use Case: Organizations & Topics

Another common use case is for teams, organizations or topics.

For example, one of our customers runs a football community. Updates about a player should also be added to their team's feed. Stream supports this by adding the activity to the player's feed, and specifying the target feeds in the "TO" field:

# The TO field ensures the activity is send to the player, match and team feed
activity_data = {:actor => 'Player:Suarez', :verb => 'foul', :object => 'Player:Ramos',
    :match => {:name => 'El Clasico', :id => 10},
    :to => ['team:barcelona', 'match:1'],
}
player_feed_1.add_activity(activity_data)
// The TO field ensures the activity is send to the player, match and team feed
activity = {'actor': 'Player:Suarez', 'verb': 'foul', 'object': 'Player:Ramos',
    'match': {'name': 'El Clasico', 'id': 10},
    'to': ['team:barcelona', 'match:1']
};
playerFeed1.addActivity(activity)
    .then(function(data) { /* on success */ })
    .catch(function(reason){ /* on failure */ });
# The TO field ensures the activity is send to the player, match and team feed
activity_data = {'actor': 'Player:Suarez', 'verb': 'foul', 'object': 'Player:Ramos',
    'match': {'name': 'El Clasico', 'id': 10},
    'to': ['team:barcelona', 'match:1']
}
player_feed_1.add_activity(activity_data)
// The TO field ensures the activity is send to the player, match and team feed
$data = [
    'actor' => 'Player:Suarez',
    'verb' => 'foul',
    'object' => 'Player:Ramos',
    'match' => ['name' => 'El Clasico', 'id' => 10],
    'to' => ['team:barcelona', 'match:1'],
];

$playerFeed1->addActivity($data);
// The TO field ensures the activity is send to the player, match and team feed
activity = Activity.builder()
        .actor("Player:Suarez")
        .verb("foul")
        .object("Player:Ramos")
        .to(Lists.newArrayList(new FeedID("team:barcelona"), new FeedID("match:1")))
        .extraField("match", ImmutableMap.of("El Classico", 10))
        .build();
playerFeed.addActivity(activity).join();
// The TO field ensures the activity is send to the player, match and team feed
activity := stream.Activity{
    Actor:  "Player:Suarez",
    Verb:   "foul",
    Object: "Player:Ramos",
    To: []string{
        "team:barcelona",
        "match:1",
    },
    Extra: map[string]interface{}{
        "match": map[string]interface{}{
            "name": "El Clasico",
            "id":   10,
        },
    },
}
_, err := playerFeed.AddActivity(activity)
if err != nil {
    panic(err)
}
// The TO field ensures the activity is sent to the player, match and team feed
var activity = new Activity("Player:Suarez", "foul", "Player:Ramos")
{
    To = new string[] { "team:barcelona", "match:1" }
};
await playerFeed1.AddActivity(activity);

By default only the user, flat, aggregated and notification feed groups are setup automatically. You'll need to go to the dashboard and create feeds called team, player and match for the above example to work.

What To Store

Stream allows you to store custom data in your activities. How much data you store in the activities is up to you. Here are a few recommendations:

  • Always send the foreign_id and time fields. Without "foreign_id" and "time" you won't be able to update activities.
  • Keep data normalized when possible to avoid complex update flows. Activity fields like actor and object often contain complex objects like users, posts or articles. Since the data for those object can be updated, it is better to store a reference (eg. their ID or URI) and replace them with the full object at read-time.
  • Store a user id and not the full user object. Otherwise if someone changes their user image or username you'll have to update tons of activities.
  • Attach metadata like tags and categories (if applicable) to your activities. This will help our data-science team optimize your feed personalization algorithms. (available on enterprise plans).
  • If you're using ranked feeds we recommend storing counts of the likes and comments. That allows you to take those factors into account when sorting feeds.
  • Activities have a max size. Storing a large amount of text is ok, storing images directly in the activity won't work, you should store the image URL instead.

The process of keeping your data normalized while working with complex objects is called enrichment.

Follow Many

Stream's Follow Many functionality gives you a fast method to follow many feeds in one go. This is convenient when importing data or on-boarding new users.

Follow Many has is a limit of 2,500 follows per request.

# Batch following many feeds
# Let timeline:1 will follow user:1, user:2 and user:3
follows = [
    {:source => 'timeline:1', :target => 'user:1'},
    {:source => 'timeline:1', :target => 'user:2'},
    {:source => 'timeline:1', :target => 'user:3'}
]
client.follow_many(follows)
// Batch following many feeds
// Let timeline:1 will follow user:1, user:2 and user:3
var follows = [
  {'source': 'timeline:1', 'target': 'user:1'},
  {'source': 'timeline:1', 'target': 'user:2'},
  {'source': 'timeline:1', 'target': 'user:3'}
];

client.followMany(follows);
# Batch following many feeds
# Let timeline:1 will follow user:1, user:2 and user:3
follows = [
    {'source': 'timeline:1', 'target': 'user:1'},
    {'source': 'timeline:1', 'target': 'user:2'},
    {'source': 'timeline:1', 'target': 'user:3'}
]
client.follow_many(follows)
# copy only the last 10 activities from every feed
client.follow_many(follows, activity_copy_limit=10)
// Batch following many feeds
// Let timeline:1 will follow user:1, user:2 and user:3
$follows = [
    ['source' => 'timeline:1', 'target' => 'user:1'],
    ['source' => 'timeline:1', 'target' => 'user:2'],
    ['source' => 'timeline:1', 'target' => 'user:3'],
];

$batcher->followMany($follows);
// Batch following many feeds
// Let timeline:1 will follow user:1, user:2 and user:3
FollowRelation[] follows = new FollowRelation[]{
        new FollowRelation("timeline:1", "user:1"),
        new FollowRelation("timeline:3", "user:2"),
        new FollowRelation("timeline:1", "user:3")
};
client.batch().followMany(follows).join();
// copy only the last 10 activities from every feed
client.batch().followMany(10, follows).join();
// Batch following many feeds
// Let timeline:1 will follow user:1, user:2 and user:3
follows := []stream.FollowRelationship{
    stream.NewFollowRelationship(timelineFeed, userFeed1),
    stream.NewFollowRelationship(timelineFeed, userFeed2),
    stream.NewFollowRelationship(timelineFeed, userFeed3),
}
err = client.FollowMany(follows)
if err != nil {
    panic(err)
}
// copy only the last 10 activities from every feed
err = client.FollowMany(follows, stream.WithFollowManyActivityCopyLimit(10))
if err != nil {
    panic(err)
}
// Batch following many feeds
// Let timeline:1 follow user:1, user:2 and user:3
var follows = new[]
{
    new Follow("timeline:1", "user:1"),
    new Follow("timeline:1", "user:2"),
    new Follow("timeline:1", "user:3")
};
await client.Batch.FollowMany(follows);

// copy only the last 10 activities from every feed
await client.Batch.FollowMany(follows, 10);

Parameters

Name Type Description Default
list The list of a follow objects e.g. [{'source': 'timeline:1', 'target': 'user:2'} ]
activity_copy_limit integer How many activities should be copied from the target feed, max 1000 300

Note: The API does not return any data.

Batch add activities

Multiple activities can be added with a single batch operation. This is very convenient when importing data to Stream.

Take a look at the example below:

activities = [
    {:actor => 'User:1', :verb => 'tweet', :object => 'Tweet:1'},
    {:actor => 'User:2', :verb => 'watch', :object => 'Movie:1'}
]
user_feed_1.add_activities(activities)
var activities = [
    {'actor': 'User:1', 'verb': 'tweet', 'object': 'Tweet:1'},
    {'actor': 'User:2', 'verb': 'watch', 'object': 'Movie:1'}
];

user1.addActivities(activities)
    .then(function(data) { /* on success */ })
    .catch(function(reason){ /* on failure */ });
activities = [
    {'actor': 'User:1', 'verb': 'tweet', 'object': 'Tweet:1'},
    {'actor': 'User:2', 'verb': 'watch', 'object': 'Movie:1'}
]
user_feed_1.add_activities(activities)
$activities = [
    ['actor' => 'User:1', 'verb' => 'tweet', 'object' => 'Tweet:1'],
    ['actor' => 'User:2', 'verb' => 'watch', 'object' => 'Movie:1'],
];

$userFeed1->addActivities($activities);
Activity[] activities = new Activity[]{
        Activity.builder()
                .actor("User:1")
                .verb("tweet")
                .object("Tweet:1")
                .build(),
        Activity.builder()
                .actor("User:2")
                .verb("watch")
                .object("Movie:1")
                .build()
};
userFeed.addActivities(activities).join();
activities := []stream.Activity{
    {Actor: "User:1", Verb: "tweet", Object: "Tweet:1"},
    {Actor: "User:2", Verb: "watch", Object: "Movie:1"},
}
_, err := userFeed.AddActivities(activities...)
if err != nil {
    panic(err)
}
var activities = new Activity[]{
    new Activity("User:1", "tweet", "Tweet:1"),
    new Activity("User:2", "watch", "Movie:1")
};

await userFeed1.AddActivities(activities);

Parameters

Name Type Description
activities list The list of activities to be added (as specified in Adding Activities)

Activity IDs

The API will return a response with a list containing the activity ids.

Note: "To" field targetting is not permitted via this endpoint. Attempting to add activities with a 'to' property will result in an error response.
Note: If you are importing your data for the first time, we suggest you to create the follow relationships after the activity import.

Batch Activity Add

Allows you to add a single activity to multiple feeds with one API request.

Batch Activity Add has a limit of 5,000 target feeds. Requests that exceed this limit will return an error.

# adds 1 activity to many feeds in one request
feeds = ['timeline:1', 'timeline:2', 'timeline:3', 'timeline:4']
activity = {:actor => "User:2", :verb => "pin", :object => "Place:42", :target => "Board:1"}
client.add_to_many(activity, feeds)
var feeds = ['timeline:1', 'timeline:2', 'timeline:3', 'timeline:4'];
var activity = { 'actor': 'User:2', 'verb': 'pin', 'object': 'Place:42', 'target': 'Board:1' };
client.addToMany(activity, feeds);
# adds 1 activity to many feeds in one request
feeds = ['timeline:1', 'timeline:2', 'timeline:3', 'timeline:4']
activity = {"actor": "User:2", "verb": "pin", "object": "Place:42", "target": "Board:1"}
client.add_to_many(activity, feeds)
// adds 1 activity to many feeds in one request
$activity = [
   'actor' => '1',
   'verb' => 'tweet',
   'object' => '1',
];

$follows = [
    ['source' => 'timeline:1', 'target' => 'user:1'],
    ['source' => 'timeline:1', 'target' => 'user:2'],
];

$batcher->addToMany($activity, $feeds);
// adds 1 activity to many feeds in one request
activity = Activity.builder()
        .actor("User:2")
        .verb("pin")
        .object("Place:42")
        .target("Board:1")
        .build();
FeedID[] feeds = new FeedID[]{
        new FeedID("timeline", "1"),
        new FeedID("timeline", "2"),
        new FeedID("timeline", "3"),
        new FeedID("timeline", "4")
};
client.batch().addToMany(activity, feeds).join();
// adds 1 activity to many feeds in one request
feeds := []stream.Feed{
    client.FlatFeed("timeline", "1"),
    client.FlatFeed("timeline", "2"),
    client.FlatFeed("timeline", "3"),
    client.FlatFeed("timeline", "4"),
}
activity := stream.Activity{
    Actor:  "User:1",
    Verb:   "pin",
    Object: "Place:42",
    Target: "Board:1",
}
err := client.AddToMany(activity, feeds)
if err != nil {
    panic(err)
}
// adds 1 activity to many feeds in one request
var feeds = new string[] { "timeline:1", "timeline:2", "timeline:3", "timeline:4" };
var activity = new Activity("User:2", "pin", "Place:42")
{
    Target = "Board:1"
};
await client.Batch.AddToMany(activity, feeds);

Parameters

Name Type Description
feeds list The list of a feeds e.g. ['user:1', 'timeline:2']
activity object The activity object (see Feed endpoint for reference)
Note: Activities added using this method are not propagated to followers. That is, any other Feeds that follow the Feed(s) listed in the API call will not receive the new Activity.

Retrieving activities by ID

Activities can be retrieved by IDs or foreign ID and time.

# retrieve two activities by ID
client.get_activities(
  ids: [
    '01b3c1dd-e7ab-4649-b5b3-b4371d8f7045',
    'ed2837a6-0a3b-4679-adc1-778a1704852d'
  ]
)

# retrieve two activities by foreign ID and timestamp
client.get_activities(
  foreign_id_times: [
    { foreign_id: 'like:1', time: '2018-07-08T14:09:36.000000' },
    { foreign_id: 'post:2', time: '2018-07-09T20:30:40.000000' }
  ]
)
// retrieve two activities by ID
client.getActivities({
    ids: [
        '01b3c1dd-e7ab-4649-b5b3-b4371d8f7045',
        'ed2837a6-0a3b-4679-adc1-778a1704852d'
    ]
}).then(function(resp) { /* ... */ });

// retrieve an activity by foreign ID and time
client.getActivities({
    foreignIDTimes: [
        { foreign_id: 'like:1', time: '2018-07-08T14:09:36.000000' },
        { foreign_id: 'post:2', time: '2018-07-09T20:30:40.000000' },
    ]
}).then(function(resp) { /* ... */ });
# retrieve two activities by ID
client.get_activities(ids=[
    '01b3c1dd-e7ab-4649-b5b3-b4371d8f7045',
    'ed2837a6-0a3b-4679-adc1-778a1704852d'
])

# retrieve an activity by foreign ID and time
client.get_activities(foreign_id_times=[
    (foreign_id, activity_time),
])
# not supported yet
// retrieve two activities by ID
client.batch().getActivitiesByID("01b3c1dd-e7ab-4649-b5b3-b4371d8f7045", "ed2837a6-0a3b-4679-adc1-778a1704852").join();

// retrieve two activities by foreign ID and timestamp
client.batch().getActivitiesByForeignID(new ForeignIDTimePair("foreignID1", new Date()), new ForeignIDTimePair("foreignID2", new Date())).join();
// retrieve two activities by ID
resp, err := client.GetActivitiesByID("01b3c1dd-e7ab-4649-b5b3-b4371d8f7045", "ed2837a6-0a3b-4679-adc1-778a1704852d")
if err != nil {
    panic(err)
}

// retrieve two activities by foreign ID and timestamp
resp, err := client.GetActivitiesByForeignID(
    stream.NewForeignIDTimePair("like:1", stream.Time{Time: /* ... */}),
    stream.NewForeignIDTimePair("post:2", stream.Time{Time: /* ... */}),
)
// retrieve two activities by ID
await client.Batch.GetActivities(new string[]
{
    "01b3c1dd-e7ab-4649-b5b3-b4371d8f7045",
    "ed2837a6-0a3b-4679-adc1-778a1704852d"
});

// retrieve two activities by foreign ID and timestamp
await client.Batch.GetActivities(null, new ForeignIDTime[]
{
    new ForeignIDTime("like:1", DateTime.Parse("2018-07-08T14:09:36.000000")),
    new ForeignIDTime("post:1", DateTime.Parse("2018-07-09T20:30:40.000000"))
});
Note: combining ID and foreign ID + time parameters is not allowed.

Parameters

Name Type Description Optional
ids string The comma separated list of activity IDs to retrieve
foreign_id_times list The list of foreign_id and time values used to retrieve activities
Note: the number of activities that can be retrieved with a single request is limited to 100.

Feed Update Discard Rules

It is possible to discard activities added to a feed based on certain rules.

Common Use Case

One common use case is when users post in a feed that one of their own notification feeds follow.

By default, the user gets a notification of their own activity. By applying a discard rule, the user's own activities can be filtered.

Parameters

The following discard rules are available:

Name Description
discard_rule_default Discard when activity actor matches the user_id of the feed
discard_rule_actor_with_colon Discard when the second part of the activity actor split on colon matches the user_id of the feed

To enable this feature, contact support with:

  • the name of the feed group
  • the name of the rule to apply

Importing Data

If you've just started using Stream, you might want to import data from your old infrastructure. Instead of using the APIs and creating your own import scripts, you can make use of our import feature. If you are on a paid plan you can import data into your app via the dashboard.

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. Upload the import file via the dashboard. The import file is limited to a maximum size of 300MB. For larger imports please contact us
  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
Note: Before uploading the import file make sure that every feed group in your import file is configured in your app.
Note: Importing data on a live app may cause high response times for requests during the import process.

Import file

The import file must be in the JSON Lines text format (newline-delimited json). That means that each line must be a valid json. More info here.

Instruction reference

Each line consists of an import instruction and must have the following elements:

Name Type Description
instruction string Instruction name. Must be a valid instruction type
feedId string The feed to import data to, required by add_activities and follow
data array The data for that particular instruction. See below

Instruction types

An import file can contain different type of instruction types. This is the list of supported instruction types:
  • add_activities
  • follow
  • reactions
  • users
  • collections

follow

An array of feed ids. As you can only follow flat feeds, make sure that all the feeds are flat feeds.

{"instruction": "follow", "feedId": "user:bob", "data": ["user:alice", "user:tim"]}

add_activities

An array of activities. The rules described in the adding activities doc apply for each activity. In addition, each activity must have the foreign_id and time fields present. The time field must have the RFC3339 format.

{"instruction": "add_activities", "feedId": "user:1", "data":[{"actor": "user:1", "verb": "like", "object": "cat:1", "foreign_id": "like:2", "time": "2017-02-01T16:03:39+00:00"},{"actor": "user:1", "verb": "share", "object": "picture:2", "foreign_id": "share:3", "time": "2017-02-01T19:03:39+00:00"}]}

collections

An array of collection entries, each line can have up to 10,000 entries. Entries must have the following fields not empty: collection, id and data.

{"instruction": "collections", "data": [{"id": "qotsa", "collection": "bands", "site": "http://www.qotsa.com/"}]}

users

An array of users, each line can have up to 10,000 entries. Entries must have the id field.

{"instruction": "users", "data": [{"id": "415", "username": "josh-homme", "band": "SO:bands:qotsa"}, {"id": "42", "username": "bob", "band": "SO:bands:bob-dylan"}]}

reactions

An array of reactions, each line can have up to 10,000 entries. Entries must include: kind, user_id, activity_id, time. If the related activity is part of the same import, activity_foreign_id and activity_time replace activity_id. Check the REST docs for the complete list of allowed field.

{"instruction": "reactions", "data":[{"kind": "like", "time": "2017-02-01T16:03:39+00:00", "user_id": "tim", "activity_id": "af781804-847e-11e4-8080-80012fb97b9e"}]}
{"instruction": "reactions", "data":[{"kind": "comment", "data": {"text": "how many imports must a man"}, "time": "2017-02-01T19:03:39+00:00", "user_id": "bob", "activity_time": "2017-02-01T16:03:38+00:00", "activity_foreign_id": "post:123"}]}

Rate Limiting

Every Application is rate limited at the API endpoint. These limits are set on a 1-minute, 15-minute and 1-hour time windows, API requests have specific rate limits. For example, reading a feed has a different limit than establishing a follow relationship.

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 Pro and Enterprise plans, Stream will review your feed group 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 Limits

API Request Calls per Minute Calls per 15 Minutes Calls per Hour
Adding Activities 1,000 12,000 48,000
Removing Activities 300 3,600 14,400
Updating Activities 250 3,000 12,000
Retrieving Activities 2,000 24,000 96,000
Following Feeds 500 6,000 24,000
Unfollowing Feeds 250 3,000 12,000
Reading Feed Followers 200 2,400 9,600
Reading Followed Feeds 500 6,000 24,000
Batch Follow 250 3,000 12,000
Collections upsert 250 3,000 12,000
Collections select 250 3,000 12,000
Collections delete many 250 3,000 12,000
Collection read 1,000 10,000 100,000
Collections add 1,000 10,000 100,000
Collections remove 1,000 10,000 100,000
Collections update 1,000 10,000 100,000
User add 1,000 10,000 100,000
User read 1,000 10,000 100,000
User update 1,000 10,000 100,000
User delete 1,000 10,000 100,000
Reaction add 1,000 10,000 100,000
Reaction read 1,000 10,000 100,000
Reaction update 1,000 10,000 100,000
Reaction delete 1,000 10,000 100,000
Image upload 250 3,000 12,000
Image processing 250 3,000 12,000
Image delete 250 3,000 12,000
File upload 250 3,000 12,000
File delete 250 3,000 12,000

Performance, Latency & Regions

You can view the current API status on the Status page.

You can expect your feed to load in about 10ms. The most common cause of slower performance is latency. You can improve this by selecting the closest data center to you.

Stream is currently available on different locations. When you create an application, you should select the region that is closes to you to get the best latency:

  • US East - Virginia: us-east
  • Europe West - Ireland: dublin
  • Asia - Singapore: singapore
  • Asia - Tokyo: tokyo

Make sure you provide your application region when initializing the client:

client = Stream::Client.new('YOUR_API_KEY','API_KEY_SECRET', 'SITE_ID', :location => 'us-east')
// connect to the us-east region
var client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', 'SITE_ID', {'location': 'us-east'});
# connect to the us-east region
import stream
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');
$client->setLocation('us-east');
Client client = Client.builder("API_KEY", "API_SECRET").region(Region.US_EAST).build();
// connect to the us-east region
client, err := stream.NewClient(
    // API key
    "YOUR_API_KEY",
    // API secret
    "API_KEY_SECRET",
    // API region
    stream.WithAPIRegion("us-east"),
)
if err != nil {
    panic(err)
}
var opts = StreamClientOptions.Default;
opts.Location = StreamApiLocation.USEast;
var client = new StreamClient("YOUR_API_KEY", "API_KEY_SECRET", opts);

Enrichment & Performance

Another common cause of slow feed performance is the enrichment step. Often you'll send references to your database in the activities. When loading your feed you need to run certain database queries to load the latest version of this data.

In cases like this there are 2 things to keep in mind:

  1. Batch all the database queries. Never run N queries for N items in the feed.
  2. Cache the lookup by primary key using a model layer cache (recommended only for large apps).
  3. Use Stream collections and user endpoints to store data and get best performance.

More information on how enrichment works can be found here.

Web, iOS & Android

Many of our users build native iOS and Android apps, web app or mobile apps with React Native.

Stream can be used directly inside a browser or mobile application. The only difference is how you initialize the API client.
Because browsers and mobile apps are not trusted environments, you must create a user token server-side and pass it to your app. This way, each user will get a unique token that allows performing actions only to their feeds and activities.

const userId = 'test-user-1';
const userToken = client.createUserToken(userId);
user_id = 'test-user-1'
client.create_user_session_token(user_id)
$user_id = 'test-user-1';
$client->createUserSessionToken($user_id);
String userID = "test-user-1";
Token token = client. frontendToken(userID);

Real-Time Updates

When integrating Stream on your web or mobile app, you can use the built-in realtime system to push updates to your users.

Realtime updates

Stream allows you to listen to feed changes in real-time directly on your mobile or web application. You can use the API client to subscribe for changes to one or many feeds; each time a new activity is added or removed, an update will be received directly.

Subscribing to changes in real-time is done via JavaScript using the stream-js client. If you are not using it already, you can obtain it here.

Once you have included the JavaScript client in your pages you can subscribe to real-time notifications this way:

Subscribe to realtime updates via API client

let notificationFeed = client.feed('notification', '1');

function callback(data) {
    console.log(data);
}

function successCallback() {
    console.log('now listening to changes in realtime');
}

function failCallback(data) {
    alert('something went wrong, check the console logs');
    console.log(data);
}

user1.subscribe(callback).then(successCallback, failCallback);

Subscribe to realtime updates using UMD script

<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/getstream/dist/js_min/getstream.js">
</script>

<script type="text/javascript">
    var client = stream.connect('YOUR_API_KEY', userToken, 'SITE_ID');
    var notificationFeed = client.feed('notification', '1');

    function callback(data) {
        console.log(data);
    }

    function successCallback() {
        console.log('now listening to changes in realtime');
    }

    function failCallback(data) {
        alert('something went wrong, check the console logs');
        console.log(data);
    }

    notificationFeed.subscribe(callback).then(successCallback, failCallback);
</script>

Stream returns the following data via the realtime endpoint:


  // Realtime notification data format
  {
      "data": {
          "deleted": "array activities",
          "new": "array activities",
          "feed": "the feed id",
          "app_id": "the app id",
          "published_at":"time in iso format",
      },
      "channel":"",
  }
  
Note: To receive real-time notifications for a feed, you need to enable the feature via the Dashboard's feed configuration page.

Besides the realtime endpoint, you can also connect to Stream's Firehose via SQS or webhooks. This is helpful if you want to send push notifications or emails based on feed activity.

React Components

If you are using React for your application, you can take advantage of Stream's react-activity-feed component library.

import React, { Component } from 'react';

import { StreamApp, StatusUpdateForm, FlatFeed } from 'react-activity-feed';
import 'react-activity-feed/dist/index.es.css';

export default class App extends Component {
  render() {
    return (
      <div style={{ width: '600px', margin: '0 auto' }}>
        <StreamApp
          apiKey="YOUR_API_KEY"
          appId="YOUR_APP_ID"
          token={userToken}
        >
          <StatusUpdateForm />
          <FlatFeed feedGroup="user" notify />
        </StreamApp>
      </div>
    );
  }
}

React Native Components

If you are using React Native for your application, you can take advantage of Stream's react-native-activity-feed component library.

import React from 'react';
import SafeAreaView from 'react-native-safe-area-view';
import {
    StreamApp,
    FlatFeed,
    Activity,
    LikeButton,
    StatusUpdateForm,
} from 'expo-activity-feed';

const CustomActivity = (props) => {
  return (
    <Activity
      {...props}
      Footer={
        <LikeButton {...props} />
      }
    />
  );
};

const App = () => {
  return (
    <SafeAreaView style={{flex: 1}} forceInset={{ top: 'always' }}>
      <StreamApp
        apiKey="YOUR_API_KEY"
        appId={APP_ID}
        token={userToken}
      >
        <FlatFeed Activity={CustomActivity} />
        <StatusUpdateForm feedGroup="timeline" />
      </StreamApp>
    </SafeAreaView>
  );
};

export default App;

iOS Components

If you are building an app with Stream on iOS and use Swift, you can soon take advantage of Stream's components library.

The library is not yet publicly available. Please contact support if you are interested in early access or want to know more about this.

Reactions

Reactions are a special kind of data that can be used to capture user interaction with specific activities. Common examples of reactions are: likes, comments and upvotes. Reactions are automatically returned to feeds' activities at read time when the reactions parameters are used.

Add reactions

Reactions are a special kind of data that can be used to capture user interaction with specific activities. Common examples of reactions are: likes, comments and upvotes. Reactions are automatically returned to feeds' activities at read time.

Reactions are always related to activities; in order to add a reaction to an activity you need to provide its ID.

Listed below are the parameters for adding reactions.

Parameters

Name Type Description Optional
kind string The type of reaction (eg. like, comment, ...)
activity_id string The ID of the activity the reaction refers to
data object Additional data to attach to the reaction
target_feeds array The feeds that should receive a notification activity
// add a like reaction to the activity with id activityId
let like = await client.reactions.add("like", activityId);

// adds a comment reaction to the activity with id activityId
let comment = await client.reactions.add("comment", activityId, {"text": "awesome post!"});
# add a like reaction to the activity with id activityId
client.reactions.add("like", activity_id, user_id="mike")

# adds a comment reaction to the activity with id activityId
client.reactions.add(
    "comment", activity_id, user_id="mike", data={"text": "awesome post!"}
)
Reaction like = new Reaction.Builder()
        .kind("like")
        .activityID(activity.getID())
        .build();

// add a like reaction to the activity with id activityId
like = client.reactions().add("john-doe", like).get();

Reaction comment = new Reaction.Builder()
        .kind("comment")
        .activityID(activity.getID())
        .extraField("text", "awesome post!")
        .build();

// adds a comment reaction to the activity with id activityId
comment = client.reactions().add("john-doe", comment).get();
// add a like reaction to the activity with id activityID
r := stream.AddReactionRequestObject{
	Kind:       "like",
	ActivityID: activityID,
}
like, err := client.Reactions().Add(r)

// adds a comment reaction to the activity with id activityId
r = stream.AddReactionRequestObject{
	Kind:       "comment",
	ActivityID: activityID,
	UserID:     "bob",
}
comment, err := client.Reactions().Add(r)
// add a like reaction to the activity with id activityId
var like = await client.Reactions.Add("like", activityId);

// adds a comment reaction to the activity with id activityId
var comment = await client.Reactions.Add("comment", activityId, "bob");

Here's a complete example:

// first let's read current user's timeline feed and pick one activity
let response = await client.feed('timeline', 'mike').get();
let activity = response.activities[0];

// then let's add a like reaction to that activity
await client.reactions.add("like", activity.id);
# first let's read current user's timeline feed and pick one activity
response = client.feed("timeline", "mike").get()
activity = response["results"][0]

# then let's add a like reaction to that activity
client.reactions.add("like", activity["id"])
// first let's read current user's timeline feed and pick one activity
List<Activity> response = client.flatFeed("timeline", "mike").getActivities().get();
Activity activity = response.get(0);

// then let's add a like reaction to that activity
client.reactions().add("john-doe", Reaction.builder()
        .kind("like")
        .activityID(activity.getID())
        .build()).join();
// first let's read current user's timeline feed and pick one activity
response, err := client.FlatFeed("timeline", "bob").GetActivities()
if err != nil {
	//...
}
activity := response.Results[0]

// then let's add a like reaction to that activity
r := stream.AddReactionRequestObject{
	Kind:       "like",
	ActivityID: activity.ID,
	UserID:     "bob",
}
result, err := client.Reactions().Add(r)
// first let's read current user's timeline feed and pick one activity
var response = await client.Feed("timeline", "bob").GetActivities();
var activity = response.FirstOrDefault();

// then let's add a like reaction to that activity
await client.Reactions.Add("like", activity.Id, "bob");

Notify other feeds

When adding a reaction, you can use the target_feeds parameter to notify a list of users about the new reaction. When specified, all targeted feeds will receive an activity containing a reference to the reaction.

// adds a comment reaction to the activity and notify Thierry's notification feed
client.reactions.add("comment", activityId, 
    {"text": "@thierry great post!"},
    ["notification:thierry"]
);
client.reactions.add(
    "comment",
    activity_id,
    user_id="mike",
    data={"text": "@thierry great post!"},
    target_feeds=["notification:thierry"],
)
Reaction comment = new Reaction.Builder()
        .kind("comment")
        .activityID(activity.getID())
        .extraField("text", "@thierry great post!")
        .build();

// adds a comment reaction to the activity and notify Thierry's notification feed
client.reactions().add("john-doe", comment, new FeedID("notification:thierry")).join();
// adds a comment reaction to the activity and notify Thierry's notification feed
r := stream.AddReactionRequestObject{
	Kind:       "comment",
	ActivityID: activityID,
	UserID:     "bob",
	Data: map[string]interface{}{
		"text": "@thierry great post!",
	},
	TargetFeeds: []string{"notification:thierry"},
}
result, err := client.Reactions().Add(r)
// adds a comment reaction to the activity and notify Thierry's notification feed
var reactionData = new Dictionary<string, object>()
{
    { "text", "@thierry great post!"},
};
var feedsToNotify = new string[] { "notification:thierry" };
await client.Reactions.Add("comment", activityId, "bob", reactionData, feedsToNotify);

Read feeds with reactions

When using reactions, it is possible to request enriched activities that include attached reactions by type and reaction counters.

Listed below are the parameters for retrieving reactions.

Parameters

Name Type Description Optional
recent boolean include the 10 most recent reactions to activities
own boolean include current user's reaction to activities
counts boolean include the total count of reaction (by kind) to activities
// read bob's timeline and include most recent reactions to all activities and their total count
client.feed('timeline', 'bob').get({ 
    reactions: { recent: true, counts: true }
});

// read bob's timeline and include most recent reactions to all activities and her own reactions
client.feed('timeline', 'bob').get({
    reactions: { own: true, recent: true, counts: true }
});
# read bob's timeline and include most recent reactions to all activities and their total count
client.feed("timeline", "bob").get(reactions={"recent": True, "counts": True})

# read bob's timeline and include most recent reactions to all activities and her own reactions
client.feed("timeline", "bob").get(
    reactions={"own": True, "recent": True, "counts": True}, user_id="bob"
)
// read bob's timeline and include most recent reactions to all activities and their total count
client.flatFeed("timeline", "bob")
        .getEnrichedActivities(new EnrichmentFlags()
                .withRecentReactions()
                .withReactionCounts()).get();

// read bob's timeline and include most recent reactions to all activities and her own reactions
client.flatFeed("timeline", "bob")
        .getEnrichedActivities(new EnrichmentFlags()
                .withOwnReactions()
                .withRecentReactions()
                .withReactionCounts()).get();
// read bob's timeline and include most recent reactions to all activities and their total count
opts := []stream.GetActivitiesOption{
	stream.WithEnrichRecentReactions(),
	stream.WithEnrichReactionCounts(),
}
client.FlatFeed("timeline", "bob").GetEnrichedActivities(opts...)

// read bob's timeline and include most recent reactions to all activities and her own reactions
opts := []stream.GetActivitiesOption{
	stream.WithEnrichOwnReactions(),
	stream.WithEnrichRecentReactions(),
	stream.WithEnrichReactionCounts(),
}
client.FlatFeed("timeline", "bob").GetEnrichedActivities(opts...)
// read bob's timeline and include most recent reactions to all activities and their total count
await client.Feed("timeline", "bob").GetEnrichedFlatActivities(GetOptions.Default.WithReaction(ReactionOption.With().Recent().Counts()));

// read bob's timeline and include most recent reactions to all activities and her own reactions
await client.Feed("timeline", "bob").GetEnrichedFlatActivities(GetOptions.Default.WithReaction(ReactionOption.With().Recent().Own()));

Retrieving reactions

You can read reactions and filter them based on their "user_id" or "activity_id" values.
Further filtering can be done with the "kind" parameter (eg. retrieve all likes by one user, retrieve all comments for one activity, ...)

Reactions are returned in descending order (newest to oldest) by default or when using id_lt[e] and in ascending order (oldest to newest) when using id_gt[e].

Parameters

Name Type Description Default Optional
activity_id string Retrieve reactions by activity_id
user_id string Retrieve reactions by user_id
reaction_id string Retrieve children reaction by reaction_id
kind string If provided it will only retrieve reactions of a certain kind (eg. "like")
limit int The number of reactions to retrieve (max: 25) 10
id_gte string Retrieve reactions created after the one with ID equal to the parameter (inclusive)
id_gt string Retrieve reactions created after the one with ID equal to the parameter
id_lte string Retrieve reactions created before the one with ID equal to the parameter (inclusive)
id_lt string Retrieve reactions before after the one with ID equal to the parameter
with_activity_data boolean Returns activity data when paginating using activity_id
// retrieve all kind of reactions for an activity
let reactions = await client.reactions.filter({
       'activity_id': 'ed2837a6-0a3b-4679-adc1-778a1704852d'
});

// retrieve first 10 likes for an activity
let response = await client.reactions.filter({
        'activity_id': 'ed2837a6-0a3b-4679-adc1-778a1704852d',
        'kind': 'like',
        'limit': 10
});

// retrieve the next 10 likes using the id_lt param
let response = await client.reactions.filter({
        'activity_id': 'ed2837a6-0a3b-4679-adc1-778a1704852d',
        'kind': 'like',
        'id_lt': 'e561de8f-00f1-11e4-b400-0cc47a024be0',
});
# retrieve all kind of reactions for an activity
reactions = client.reactions.filter(
    activity_id="ed2837a6-0a3b-4679-adc1-778a1704852d"
)

# retrieve first 10 likes for an activity
response = client.reactions.filter(
    activity_id="ed2837a6-0a3b-4679-adc1-778a1704852d", kind="like", limit=10
)

# retrieve the next 10 likes using the id_lt param
response = client.reactions.filter(
    activity_id="ed2837a6-0a3b-4679-adc1-778a1704852d",
    kind="like",
    id_lt="e561de8f-00f1-11e4-b400-0cc47a024be0",
)
// retrieve all kind of reactions for an activity
List<Reaction> reactions = client.reactions().filter(LookupKind.ACTIVITY, "ed2837a6-0a3b-4679-adc1-778a1704852d").get();

// retrieve first 10 likes for an activity
reactions = client.reactions()
        .filter(LookupKind.ACTIVITY,
                "ed2837a6-0a3b-4679-adc1-778a1704852d",
                new Filter().limit(10),
                "like").join();

// retrieve the next 10 likes using the id_lt param
reactions = client.reactions()
        .filter(LookupKind.ACTIVITY,
                "ed2837a6-0a3b-4679-adc1-778a1704852d",
                new Filter().idLessThan("e561de8f-00f1-11e4-b400-0cc47a024be0"),
                "like").join();
var response *stream.FilterReactionResponse
var err error

// retrieve all kind of reactions for an activity
response, err = client.Reactions().Filter(stream.ByActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d"))

// retrieve first 10 likes for an activity
response, err = client.Reactions().Filter(stream.ByActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d").ByKind("like"), stream.WithLimit(10))

// retrieve the next 10 likes using the id_lt param
filterAttribute := stream.ByActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d").ByKind("like")
pagination := stream.WithIDLT("e561de8f-00f1-11e4-b400-0cc47a024be0")

response, err = client.Reactions().Filter(filterAttribute, pagination)
// retrieve all kind of reactions for an activity
var reactions = await client.Reactions.Filter(ReactionFiltering.Default, ReactionPagination.By.ActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d"));


// retrieve first 10 likes for an activity
var response = await client.Reactions.Filter(ReactionFiltering.Default.WithLimit(10), ReactionPagination.By.ActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d").Kind("like"));

// retrieve the next 10 likes using the id_lt param
var filter = ReactionFiltering.Default.WithFilter(FeedFilter.Where().IdLessThan("e561de8f-00f1-11e4-b400-0cc47a024be0"));
var pagination = ReactionPagination.By.ActivityID("ed2837a6-0a3b-4679-adc1-778a1704852d").Kind("like");
var response2 = await client.Reactions.Filter(filter, pagination);

Child reactions

A reaction can also be added to another reaction; in this case a child reaction is created. Child reactions are created in the same way as regular reactions but have a few important differences:

  • Child reactions are not part of the parent activity counts
  • Child reactions cannot have child reactions
  • Child reactions are only returned when the parent is returned
  • In order to paginate over reactions, you need to filter using the parent reaction ID
  • Recent children reactions and their counts are added to the parent reaction body

// adds a like to the previously created comment
client.reactions.addChild('like', comment);
# adds a like to the previously created comment
client.reactions.add_child("like", comment, user_id="mike")
Reaction reaction = Reaction.builder().kind("like").build();
// adds a like to the previously created comment
reaction = client.reactions().addChild("john-doe", comment.getId(), reaction).get();
// adds a like to the previously created comment
r := stream.AddReactionRequestObject{
	Kind:       "like",
	ActivityID: activity.ID,
	UserID:     "bob",
}
result, err := client.Reactions().AddChild(comment.ID, r)
// adds a like to the previously created comment
await client.Reactions.AddChild(comment, "like", activity.Id, "bob");

Updating Reactions

Reactions can be updated by providing activity ID and reaction ID parameters.

Changes to reactions are propagated to all notified feeds; if the target_feeds list is updated, notifications will be added and removed accordingly.

Parameters

Name Type Description Optional
reaction_id string The ID of the reaction
data object reaction data
target_feeds string The list of feeds that should receive a copy of the reaction
client.reactions.update(reactionId, {"text": "love it!"});
client.reactions.update(reactionId, {"text": "love it!"});
client.reactions().update(Reaction.builder()
        .id(reaction.getId())
        .extraField("text", "love it!")
        .build());
result, err := client.Reactions().Update(reactionID, map[string]interface{}{"text": "love it!"}, nil)
var reactionData = new Dictionary<string, object>()
{
    { "text", "love it!"}
};
await client.Reactions.Update(reactionId, reactionData);

Removing Reactions

Reactions can be removed by their ID

client.reactions.delete(reactionId);
client.reactions.delete(reaction_id);
client.reactions().delete(reaction.getId()).join();
err := client.Reactions().Delete(reactionID)
await client.Reactions.Delete(reactionId);

Collections

Collections enable you to store information to Stream and use it inside your feeds and/or to provide additional data for the personalized endpoints. Examples include products, articles but any unstructured object (eg. JSON) is a good match for collections. Collection entries can be embedded inside activities and used to store nested data inside activities. When doing so Stream will automatically enrich your activities with the current version of the data (see later section). Collection endpoints can be used both client-side and server-side with the exception of the batch methods that are only available server-side.

Adding collection entries

This method allows you to create a new entry on a named collection.

Parameters

Name Type Description Optional
collection string The name of the collection
entry_id string The id of the entry, if not given an ID will be generated by Stream
data object The data related to the user
client.collections.add('food', 'cheese-burger', {name: "Cheese Burger", rating: "4 stars"})

// if you don't have an id on your side, just use null as the ID and Stream will generate a unique ID
client.collections.add("food", null, {name: "Cheese Burger", rating: "4 stars"})
client.collections.add("food", {"name": "Cheese Burger", "rating": "4 stars"}, id="cheese-burger")

# if you don't have an id on your side, Stream will generate a unique ID
client.collections.add("food", {"name": "Cheese Burger", "rating": "4 stars"})
client.collections().add("food", new CollectionData("cheese-burger")
        .set("name", "Cheese Burger")
        .set("rating", "4 stars")).join();

// if you don't have an id on your side, just use null as the ID and Stream will generate a unique ID
client.collections().add("food", new CollectionData()
        .set("name", "Cheese Burger")
        .set("rating", "4 stars")).join();
obj := stream.CollectionObject{
	ID: "cheese-burger",
	Data: map[string]interface{}{
		"name":   "Cheese Burger",
		"rating": "4 stars",
	},
}
collection, err := client.Collections().Add("food", obj)

//if you don't have an id on your side, Stream will generate a unique ID

obj := stream.CollectionObject{
	Data: map[string]interface{}{
		"name":   "Cheese Burger",
		"rating": "4 stars",
	},
}
collection, err := client.Collections().Add("food", obj)
var collectionData = new GenericData();
collectionData.SetData("name", "Cheese burger");
collectionData.SetData("rating", "4 stars");
await client.Collections.Add("food", collectionData, "cheese-burger");

// if you don't have an id on your side, just use null as the ID and Stream will generate a unique ID
await client.Collections.Add("food", collectionData);

Retrieving collection entries

After adding an entry to a collection you can also retrieve it easily by its ID.

Parameters

Name Type Description
collection string The name of the collection
entry_id string The id of the entry
let response = client.collection.get('food', 'cheese-burger');
client.collection.get("food", "cheese-burger")
CollectionData collection = client.collections().get("food", "cheese-burger").join();
collection, err := client.Collections().Get("food", "cheese-burger")
await client.Collections.Get("food", "cheese-burger");

Deleting collection entries

An entry can also be removed from a collection

Name Type Description
collection string The name of the collection
entry_id string The id of the entry
let response = client.collections.delete('food', 'cheese-burger');
collections.delete("food", "cheese-burger")
client.collections().delete("food", "cheese-burger").join();
err := client.Collections().Delete("food", "cheese-burger")
await client.Collections.Delete("food", "cheese-burger");
Note: When you delete an entry from a collection any references will be converted to a missing reference error when reading feeds with enrichment.

Updating collection entries

A collection's entry data can be updated. Updates are propagated instantly to all activities embedding the entry.

Parameters

Name Type Description
collection string The name of the collection
entry_id string The id of the entry
data object The data related to the user
client.collections.update('food', 'cheese-burger', {name: "Cheese Burger", rating: "1 star"});
client.collections.update("food", "cheese-burger", {"name": "Cheese Burger", "rating": "1 star"})
client.collections().update("food", new CollectionData("cheese-burger")
        .set("name", "Cheese Burger")
        .set("rating", "1 star")).join();
collection, err := client.Collections().Update("food", "cheese-burger", map[string]interface{}{
	"name":   "Cheese Burger",
	"rating": "1 star",
})
var collectionData = new GenericData();
collectionData.SetData("name", "Cheese Burger");
collectionData.SetData("rating", "1 star");
await client.Collections.Update("food", "cheese-burger", collectionData);

Collections Batch Endpoints

You can use batch endpoints to insert, delete or read multiple entries at once. These three endpoints are only available when running Stream on your backend.

Upsert

Upsert allows you to insert or update up to 1000 entries with a single API call. The payload is an array of entries.
Entries that already exist (same ID) will be updated with the new version.

Name Type Description Optional
collection string The name of the collection
entry_id string The id of the entry, if not given an ID will be generated by Stream
data object The data related to the user
client.collections.upsert('visitor', [{id: '123', name: 'johndoe', favorite_color: 'blue'}])
await client.collections.upsert('visitor', [
    {id: '123', name: 'John', favorite_color: 'blue'},
    {id: '124', name: 'Jane', favorite_color: 'purple', interests: ['fashion', 'jazz']}
]);
client.collections.upsert(
    "visitor", [{"id": "123", "username": "johndoe", "favorite_color": "blue"}]
)
$client->collections()->upsert('visitor', [
    [
        'id' => '123',
        'username' => 'johndoe',
        'favorite_color' => 'blue',
    ]
]);
client.collections().upsert("visitor",
        new CollectionData("123")
                .set("name", "John")
                .set("favorite_color", "blue"),
        new CollectionData("124")
                .set("name", "Jane")
                .set("favorite_color", "purple")
                .set("interests", Lists.newArrayList("fashion", "jazz"))).join();
object := stream.CollectionObject{
	ID:   "123",
	Data: map[string]interface{}{
		"name": "johndoe",
		"favorite_color": "blue",
	},
}
err = client.Collections().Upsert("visitor", object)
if err != nil {
	panic(err)
}
var obj = new CollectionObject("123");
obj.SetData("name", "visitor");
obj.SetData("favorite_color", "blue");
await client.Collections.Upsert("user", obj);

Select

Allows you to retrieve up to 1000 entries by ID.

Name Type Description Optional
foreign_ids list of string The list of collection:id entries to retrieve (eg. ["visitor:123", "visitor:124"])
data object The data related to the user
# select the entries with ID 123 and 124 from items collection
objects = client.collections.get('items', ['123', '124'])
// select the entries with ID 123 and 124 from items collection
let objects = await client.collections.select('items', ['123', '124']);
# select the entries with ID 123 and 124 from items collection
objects = client.collections.select('items', ["123", "124"])
// select the entries with ID 123 and 124 from items collection
$objects = $client->collections()->select('items', ['123', '124']);
// select the entries with ID 123 and 124 from items collection
List<CollectionData> objects = client.collections().select("items", "123", "124").join();
// select the entries with ID 123 and 124 from items collection
objects, err := client.Collections().Select("items", "123", "124")
if err != nil {
	panic(err)
}
// select the entries with ID 123 and 124 from items collection
var objects = await client.Collections.Select("items", "123", "124");

Delete Many

Allows you to delete up to 1000 entries by ID.

Name Type Description Optional
collection string The name of the collection
ids list of strings The list of ids to remove
# delete the entries with ID 123 and 124 from visitor collection
client.collections.delete('visitor', ['123', '124'])
// delete the entries with ID 123 and 124 from visitor collection
client.collections.deleteMany('visitor', ['123', '124']);
# delete the entries with ID 123 and 124 from visitor collection
response = client.collections.delete_many('visitor', ["123", "124"])
// delete the entries with ID 123 and 124 from visitor collection
$response = $client->collections()->delete('visitor', ["123", "124"]);
// delete the entries with ID 123 and 124 from visitor collection
client.collections().deleteMany("visitor", "123", "124").join();
// delete the entries with ID 123 and 124 from visitor collection
err = client.Collections().DeleteMany("visitor", "123", "124")
if err != nil {
	panic(err)
}
// delete the entries with ID 123 and 124 from visitor collection
await client.Collections.Delete("visitor", "123", "124");
Note: When you delete an entry from a collection any references will be converted to a missing reference error when reading feeds with enrichment.

Enrichment of collection entries

Objects stored inside collections can be embedded inside activities or user objects. This allows you to integrate with Stream without building a complex integration with another database.
Stream's collections can be used as your data source for data enrichment.

// first we add our object to the food collection
let cheeseBurger = await client.collections.add('food', '123', {
    name:        'Cheese Burger',
    ingredients: ['cheese', 'burger', 'bread', 'lettuce', 'tomato'],
});

// the object returned by .add can be embedded directly inside of an activity
await userFeed.addActivity({actor: client.currentUser, verb: 'grill', object: cheeseBurger});

// if we now read the feed, the activity we just added will include the entire full object
await userFeed.get();

// we can then update the object and Stream will propagate the change to all activities
await cheeseBurger.update({
    name:        'Amazing Cheese Burger',
    ingredients: ['cheese', 'burger', 'bread', 'lettuce', 'tomato']
});
# first we add our object to the food collection
cheese_burger = client.collections.add(
    "food",
    data={
        "name": "Cheese Burger",
        "ingredients": ["cheese", "burger", "bread", "lettuce", "tomato"],
    },
)

# then we embed a reference to the entry we created before
user_feed.addActivity(
    {
        "actor": "jim",
        "verb": "grill",
        "object": client.collections.create_reference(
            "food", cheese_burger["id"]
        ),
    }
)

# if we now read the feed, the activity we just added will include the entire full object
user_feed.get(enrich=True)
// first we add our object to the food collection
CollectionData cheeseBurger = client.collections().add("food", new CollectionData("123")
        .set("name", "Cheese Burger")
        .set("ingredients", Lists.newArrayList("cheese", "burger", "bread", "lettuce", "tomato"))).join();

// the object returned by .add can be embedded directly inside of an activity
userFeed.addActivity(Activity.builder()
        .actor(createUserReference("john-doe"))
        .verb("grill")
        .object(createCollectionReference(cheeseBurger.getCollection(), cheeseBurger.getID()))
        .build()).join();

// if we now read the feed, the activity we just added will include the entire full object
userFeed.getEnrichedActivities();

// we can then update the object and Stream will propagate the change to all activities
client.collections().update(cheeseBurger.getCollection(), cheeseBurger
        .set("name", "Amazing Cheese Burger")
        .set("ingredients", Lists.newArrayList("cheese", "burger", "bread", "lettuce", "tomato"))).join();
//first we add our object to the food collection
obj := stream.CollectionObject{
	Data: map[string]interface{}{
		"name":        "Cheese Burger",
		"ingredients": []string{"cheese", "burger", "bread", "lettuce", "tomato"},
	},
}
cheeseBurger, err := client.Collections().Add("food", obj)

//then we embed a reference to the entry we created before
activity := stream.Activity{
	Actor:  "jim",
	Verb:   "grill",
	Object: client.Collections().CreateReference("food", cheeseBurger.ID),
}
userFeed.AddActivity(activity)

//if we now read the feed, the activity we just added will include the entire full object
enrichedResponse, err := userFeed.GetEnrichedActivities()
// first we add our object to the food collection
var collectionData = new GenericData();
collectionData.SetData("name", "Cheese Burger");
collectionData.SetData("ingredients", new string[] { "cheese", "burger", "bread", "lettuce", "tomato" });
var cheeseBurger = await client.Collections.Add("food", collectionData, "123");

// the object returned by .add can be embedded directly inside of an activity
var activity = new Activity("jim", "grill", cheeseBurger.Ref("food"));
await userFeed.AddActivity(activity);

// if we now read the feed, the activity we just added will include the entire full object
await userFeed.GetEnrichedFlatActivities();

References

When you add a user or a collection object to an activity, Stream stores the unique reference and replaces it at read time.
In some complex cases, you need to be able to generate a reference to an existing object and embed that inside of an activity.

// First create a collection entry with upsert api
await client.collections.upsert('food', [
    {id: 'cheese-burger', name: 'Cheese Burger'}
]);

// Then create a user
await client.user("john-doe").create({
    name: "John Doe", 
    occupation: "Software Engineer",
    gender: 'male'
});

// Since we know their IDs we can create references to both without reading from APIs
const cheeseBurgerRef = client.collections.createReference('food', 'cheese-burger');
const johnDoeRef = client.createUserReference('john-doe');

// And then add an activity with these references
client.feed("user", "john").addActivity({
    actor: johnDoeRef,
    verb: 'eat',
    object: cheeseBurgerRef,
});
# First create a collection entry with upsert api
client.collections.upsert(
    "food", [{"id": "cheese-burger", "name": "Cheese Burger"}]
)

# Then create a user
client.users.add(
    "john-doe",
    {"name": "John Doe", "occupation": "Software Engineer", "gender": "male"},
)

# Since we know their IDs we can create references to both without reading from APIs
cheese_burger_ref = client.collections.create_reference("food", "cheese-burger")
john_doe_ref = client.users.create_reference("john-doe")

# And then add an activity with these references
client.feed("user", "john").add_activity(
    {"actor": john_doe_ref, "verb": "eat", "object": cheese_burger_ref}
)
// First create a collection entry with upsert api
client.collections().upsert("food", new CollectionData().set("name", "Cheese Burger")).join();

// Then create a user
client.user("john-doe").create(new Data()
        .set("name", "John Doe")
        .set("occupation", "Software Engineer")
        .set("gender", "male")).join();

// Since we know their IDs we can create references to both without reading from APIs
String cheeseBurgerRef = createCollectionReference("food", "cheese-burger");
String johnDoeRef = createUserReference("john-doe");

client.flatFeed("user", "john").addActivity(Activity.builder()
        .actor(johnDoeRef)
        .verb("eat")
        .object(cheeseBurgerRef)
        .build()).join();
// First create a collection entry with upsert api
obj := stream.CollectionObject{
	ID: "cheese-burger",
	Data: map[string]interface{}{
		"name": "Cheese Burger",
	},
}
client.Collections().Upsert("food", obj)

// Then create a user
user := stream.User{
	ID: "john-doe",
	Data: map[string]interface{}{
		"name":       "John Doe",
		"occupation": "Software Engineer",
		"gender":     "male",
	},
}
client.Users().Add(user, false)

// Since we know their IDs we can create references to both without reading from APIs
cheeseBurgerRef := client.Collections().CreateReference("food", "cheese-burger")
johnDoeRef := client.Users().CreateReference("john-doe")

// And then add an activity with these references
activity := stream.Activity{
	Actor:  johnDoeRef,
	Verb:   "eat",
	Object: cheeseBurgerRef,
}
client.FlatFeed("user", "john").AddActivity(activity)
// First create a collection entry with upsert api
var collectionObj = new CollectionObject("cheese-burger");
collectionObj.SetData("name", "Cheese Burger");
await client.Collections.Upsert("food", collectionObj);

// Then create a user
var userData = new Dictionary<string, object>()
{
    {"name", "John Doe"},
    {"occupation", "Software Engineer"},
    {"gender", "male"}
};
var user = await client.Users.Add("john-doe", userData);

// Since we know their IDs we can create references to both without reading from APIs
var cheeseBurgerRef = Collections.Ref("food", "cheese-burger");
var johnDoeRef = Users.Ref("john-doe");

// And then add an activity with these references
var activity = new Activity(johnDoeRef, "eat", cheeseBurgerRef);
await client.Feed("user", "john").AddActivity(activity);

Users

Stream allows you to store user information and embed them inside activities or use them for personalization.
When stored in activities, users are automatically enriched by Stream.

let client = stream.connect('YOUR_API_KEY', userToken, 'APP_ID');

// ensure the user data is stored on Stream
await client.setUser({
    name: "John Doe", 
    occupation: "Software Engineer",
    gender: 'male'
});
# The Python API client exposes users methods on client.users
# when adding reactions, collections or reading feeds with enrichment
# you can provide `user_id` as argument
// Java API client exposes users methods on client.user object
// when adding reactions, collections you can provide `userID` as argument

Adding users

This endpoint allows you to insert a new user.

Parameters

Name Type Description
id string The unique identifier for the new user (eg. username, user id, ...)
data object The data related to the user
// create a new user, if the user already exist an error is returned
client.user("john-doe").create({
    name: "John Doe", 
    occupation: "Software Engineer",
    gender: 'male'
});

// get or create a new user, if the user already exist the user is returned
client.user("john-doe").getOrCreate({
    name: "John Doe", 
    occupation: "Software Engineer",
    gender: 'male'
});
# create a new user, if the user already exist an error is returned
client.users.add(
    "john-doe",
    {"name": "John Doe", "occupation": "Software Engineer", "gender": "male"},
)

# get or create a new user, if the user already exist the user is returned
client.users.add(
    "john-doe",
    {"name": "John Doe", "occupation": "Software Engineer", "gender": "male"},
    get_or_create=True,
)
// create a new user with the name in userdata
$user = $client->users()->add('42', array('name' => 'Arthur Dent'));
// get OR create the user
$user = $client->users()->add('42', array('name' => 'Arthur Dent'), true);
// create a new user, if the user already exist an error is returned
client.user("john-doe").create(new Data()
        .set("name", "John Doe")
        .set("occupation", "Software Engineer")
        .set("gender", "male")).join();

// get or create a new user, if the user already exist the user is returned
client.user("john-doe").getOrCreate(new Data()
        .set("name", "John Doe")
        .set("occupation", "Software Engineer")
        .set("gender", "male")).join();
// create a new user, if the user already exist an error is returned
userData := stream.User{
	ID: "john-doe",
	Data: map[string]interface{}{
		"name":       "John Doe",
		"occupation": "Software Engineer",
		"gender":     "male",
	},
}
user, err := client.Users().Add(userData, false)

// get or create a new user, if the user already exist the user is returned
user, err = client.Users().Add(userData, true)
// create a new user, if the user already exist an error is returned
var userData = new Dictionary<string, object>()
{
    {"name", "John Doe" },
    {"occupation", "Software Engineer"},
    {"gender", "male"},
};
await client.Users.Add("john-doe", userData);

// get or create a new user, if the user already exist the user is returned
await client.Users.Add("john-doe", userData, true);

Retrieving users

Allows you to retrieve a user by its id.

client.user('123').get();
client.users.get("123")
$client->users()->get('42');
client.user("123").get().join();
user, err := client.Users().Get("123")
await client.Users.Get("123");

Removing users

Removes a user by its ID.

client.user('123').delete();
client.users.delete("123")
$client->users()->delete('42');
client.user("123").delete().join();
err := client.Users().Delete("123")
await client.Users.Delete("123");
Note: When you delete a user be converted to a missing reference error when enriching.

Updating users

Update a user by its ID

Parameters

Name Type Description
id string The ID of the user to update
data object The data related to the user
client.user('123').update({name: "Jane Doe", occupation: "Software Engineer", gender: 'female'});
client.users.update("123",
    {"name": "Jane Doe", "occupation": "Software Engineer", "gender": "female"}
)
$user =  $client->users()->update('42', array('name' => 'Arthur Dent');
client.user("123").update(new Data()
        .set("name", "Jane Doe")
        .set("occupation", "Software Engineer")
        .set("gender", "female")).join();
data := map[string]interface{}{
	"name":       "John Doe",
	"occupation": "Software Engineer",
	"gender":     "female",
}
user, err := client.Users().Update("123", data)
var userData = new Dictionary<string, object>()
{
    {"name", "Jane Doe" },
    {"occupation", "Software Engineer"},
    {"gender", "female"},
};
await client.Users.Update("123", userData);

Introduction to Personalization and Analytics

Personalization is a powerful feature. It enables you to leverage machine learning to optimize what's shown in the feed. The 5 most common use cases are:

Famous examples of discovery feeds are Instagram's explore section and Pinterest's main feed. Edge rank is used by Facebook and Linkedin. Stream uses 3 data sources for personalization:

1. Feeds & Follows

The best way to understand how feeds and follows work is to try our 5 minute interactive tutorial. We also have extensive documentation which provides detailed information on the API.

2. Analytics

The purpose of analytics is to track which activities a user is looking at and what they are engaging with. Basically, you want to track everything that indicates a user's interest in something. Common examples include:

  • Clicking on a link
  • Likes or comments
  • Sharing an activity
  • Viewing another user's profile page
  • Search terms

The events and data you want to track is often different than what you traditionally track in Google Analytics or Mixpanel. Stream's analytics is designed to run side by side your existing analytics solution.

3. Collections

Collections enable you to sync information to Stream that's not captured by analytics or feeds. Common examples include user profiles and product information.

Tutorial - Instagram style personalization

This tutorial will explain how to build Instagram style personalization using Stream. The underlying tech is similar to that of a Pinterest style feed, Etsy’s e-commerce recommendations, email digests (such as Quora) or YouTube’s content suggestions.

The goal of this tutorial is to build a feed similar to Instagram's 'Explore' feed. The content is based on what type of images you've engaged with in the past as well as graph analysis. For example, if you often click on snowboarding pictures it will learn that you like snowboarding and display related posts.

Step 1: Test Data

Let's get started. As a first step we'll want to insert test data.

# Step 1: Setup the test data
require 'stream'
require 'httparty'

client = Stream::Client.new('YOUR_API_KEY', 'API_KEY_SECRET')
activities = JSON.parse(HTTParty.get('http://bit.ly/test-activities-gist'))
client.feed('user',  'global').add_activities(activities)
// Step 1: Setup the test data
let stream = require('getstream');
let request = require('request');

var API_KEY = process.env.STREAM_API_KEY
  , API_SECRET = process.env.STREAM_API_SECRET;

if (! API_KEY || ! API_SECRET) {
    throw new Error('Expected STREAM_API_KEY, STREAM_API_SECRET env vars');
}

let client = stream.connect(API_KEY, API_SECRET);

// 1. Setup the test data
request.get(
  'http://bit.ly/test-activities-gist',
  (error, response, body) => {
    if (error) {
      console.log(error);
    }
    else {
      let activities = JSON.parse(body);
      client.feed('user', 'global').addActivities(activities);
    }
});
# Step 1: Setup the test data
import stream
import requests
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET')

activities = requests.get('http://bit.ly/test-activities-gist').json()
for activity in activities:
    activity_response = client.feed('user', 'global').add_activity(activity)
    print(activity_response)
// Step 1: Setup the test data
$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');

$content = file_get_contents('http://bit.ly/test-activities-gist');
$activities = json_decode($content, true);

$response = $client->feed('user', 'global')->addActivities($activities);
echo json_encode($response) . "\n";
// 1. Setup the test data
import stream "gopkg.in/GetStream/stream-go2.v1"

client, err := stream.NewClient("YOUR_API_KEY", "API_KEY_SECRET")
if err != nil {
    panic(err)
}

resp, err := http.Get("http://bit.ly/test-activities-gist")
if err != nil {
    panic(err)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
    panic(err)
}
defer resp.Body.Close()
var activities []stream.Activity
err = json.Unmarshal(content, &activities)
if err != nil {
    panic(err)
}
_, err = client.FlatFeed("user", "global").AddActivities(activities...)
if err != nil {
    panic(err)
}

If you run the above snippet you'll insert 3 activities with the following images and tags:

#tree #snow #winter #landscape
#beach #van #surfing #travel
#dog #puppy #running #labrador

Step 2: Analytics

For this tutorial we'll only track clicks. If you're building a production application you'll want to track any type of event that signals a user's interest. The example below shows how to perform the analytics tracking if a user clicks on the 2nd image. Analytics tracking is normally done client-side, in this case we are using the JS analytics client.

// set the current user
client.setUser({id: 123, alias: 'john'});
// track a click on the 2nd picture
var engagement = {
  // the label for the engagement, ie click, retweet etc.
  'label': 'click',
  // the ID of the content that the user clicked
  'content': {
    'foreign_id': 'picture:2'
  },
  // score between 0 and 100 indicating the importance of this event 
  // IE. a like is typically a more significant indicator than a click
  'score': 15,
  // (optional) the position in a list of activities
  'position': 2
};

client.trackEngagement(engagement);
// track a click on the 2nd picture
client.analytics().trackEngagement(Engagement.builder()
        // set the current user
        .userData(new UserData("123", "john"))
        // the ID of the content that the user clicked
        .content(new Content("picture:2"))
        // the label for the engagement, ie click, retweet etc.
        .label("click")
        // score between 0 and 100 indicating the importance of this event 
        // IE. a like is typically a more significant indicator than a click
        .boost(15)
        // (optional) the position in a list of activities
        .position(3)
        .build());

The example above uses a score of 15. The score is a number between 0 and 100 and indicates the importance of an event. A click is typically less important than a like or a share, so give those events a higher score.

Step 3: Understanding Features

Those clicks give us a hint that the user is interested in that activity. There are many features attached to a given activity. For this example, we'll take a look at the features attached to the 2nd activity:

# Analyze which features activity with foreign id picture:2 has
features = client.personalization.get('analyze_features', foreign_id: 'picture:2')
puts features
// Analyze which features activity with foreign ID picture:2 has
let params = {foreign_id: 'picture:2'};
client.personalization.get('analyze_features', params).then(
  (resolution) => console.log(resolution["response"]["data"]),
  (rejection) => console.log(rejection)
);
# Analyze which features activity with foreign id picture:2 has
features = client.personalization.get('analyze_features', foreign_id='picture:2')
print(features)
// Analyze which features activity with foreign id picture:2 has
$features = $client->personalization()->get('analyze_features', ['foreign_id' => 'picture:2']);

print_r($features);
client.personalization().get("analyze_features",ImmutableMap.of("foreign_id", "picture:2"));
// Analyze which features activity with foreign id picture:2 has
featuresResp, err := client.Personalization().Get("analyze_features", map[string]interface{}{"foreign_id": "picture:2"})
if err != nil {
	// ...
}

Once you've ran the code, ask yourself why the user is interested in this picture. Is it because the picture is about surfing? Maybe it indicates that he/she likes the post since it's created by a friend? One click doesn't give us significant data, but as the user keeps returning to your app, an interest profile starts to form.

Step 4: Follows

Personalization works best if you combine explicit follow relationships with analytics events. The example below shows how to create a follow relationship.

# John follows user conner3400
client.feed('timeline', 'john').follow('user', 'conner3400')
// John follows user conner3400
john = client.feed('timeline', 'john');
john.follow('user', 'conner3400');
# John follows user conner3400
client.feed('timeline', 'john').follow('user', 'conner3400')
// John follows user conner3400
$client->feed('timeline', 'john')->follow('user', 'conner3400');
// John follows user conner3400
client.flatFeed("timeline", "john").follow(client.flatFeed("user", "conner3400"));
// John follows user conner3400
john := client.FlatFeed("timeline", "john")
conner := client.FlatFeed("user", "conner3400")
err = john.Follow(conner)
if err != nil {
	panic(err)
}

Step 5: Reading the Personalized Feed

With both follows and analytics in place we now have the ability to read the personalized feed.

activities = client.personalization.get('personalized_feed', feed_slug: 'timeline', user_id: 'john')
puts activities
params = {
  user_id: 'john',
  feed_slug: 'timeline'
};
client.personalization.get('personalized_feed', params).then(
  (resolution) => console.log(resolution["response"]["data"]),
  (rejection) => console.log(rejection)
);
activities = client.personalization.get('personalized_feed', feed_slug='timeline', user_id='john')
print(activities)
$activities = $client->personalization()->get('personalized_feed', ['feed_slug' => 'timeline', 'user_id' => 'john']);

print_r($activities);
client.personalization().get("personalized_feed", new ImmutableMap.Builder<String, Object>()
        .put("user_id", "john")
        .put("feed_slug", "timeline")
        .build());
resp, err = client.Personalization().Get("personalized_feed", map[string]interface{}{
	"feed_slug": "timeline",
	"user_id":   "john",
})
if err != nil {
	// ...
}

Step 6: Follow Suggestions

With the follow relationships in place, we have enough data to create follow suggestions.

suggestions = client.personalization.get('follow_recommendations', user_id: 'john', target_feed_slug: 'user', source_feed_slug: 'timeline')
puts suggestions
params = {
  user_id: 'john',
  source_feed_slug: 'timeline',
  target_feed_slug: 'user'
};
client.personalization.get('follow_recommendations', params).then(
  (resolution) => console.log(resolution["response"]["data"]),
  (rejection) => console.log(rejection)
);
suggestions = client.personalization.get('follow_recommendations', target_feed_slug='user', user_id='john', source_feed_slug='timeline')
print(suggestions)
$suggestions = $client->personalization()->get('follow_recommendations', [
    'user_id' => 'john',
    'source_feed_slug' => 'timeline',
    'target_feed_slug' => 'user',
]);

print_r($suggestions);
Map<String, Object> suggestions = client.personalization().get("follow_recommendations", new ImmutableMap.Builder<String, Object>()
        .put("user_id", "john")
        .put("source_feed_slug", "timeline")
        .put("target_feed_slug", "user")
        .build()).get();
resp, err = client.Personalization().Get("follow_recommendations", map[string]interface{}{
	"user_id":          "john",
	"source_feed_slug": "timeline",
	"target_feed_slug": "user",
})
if err != nil {
	// ...
}

This tutorial explained a simplified version of personalization for your app. Note: these algorithms need to be customized for each enterprise customer. Contact our data science team to learn how personalization can enhance your app.

Personalized Feeds

Personalization is custom for every enterprise customer of Stream. The SDK exposes a flexible GET method to enable you to easily make authenticated get requests to personalization:

# Read the personalization feed for a given user
client.personalization.get('personalized_feed', user_id: 123, feed_slug: 'timeline')

# Our data science team will typically tell you which endpoint to use
client.personalization.get('discovery_feed', user_id: 123, source_feed_slug: 'timeline', target_feed_slug: 'user')
# Read the personalized feed for a given user
params = {user_id: '123', feed_slug: 'timeline'};
client.personalization.get('personalized_feed', params).then(
  (resolution) => console.log(resolution["response"]["data"]),
  (rejection) => console.log(rejection)
);

# Our data science team will typically tell you which endpoint to use
params = {user_id: '123', source_feed_slug: 'timeline', target_feed_slug: 'user'}
client.personalization.get('discovery_feed', params).then(
  (resolution) => console.log(resolution["response"]["data"]),
  (rejection) => console.log(rejection)
);
# Read the personalized feed for a given user
client.personalization.get('personalized_feed', user_id=123, feed_slug='timeline')

# Our data science team will typically tell you which endpoint to use
client.personalization.get('discovery_feed', user_id=123, source_feed_slug='timeline', target_feed_slug='user')
// Read the personalized feed for a given user
$client->personalization()->get('personalized_feed', [
    'user_id' => 123,
    'feed_slug' => 'timeline',
]);

// Our data science team will typically tell you which endpoint to use
$client->personalization->get('discovery_feed', [
    'user_id' => 123,
    'source_feed_slug' => 'timeline',
    'target_feed_slug' => 'user',
]);
// Read the personalization feed for a given user
client.personalization().get("personalized_feed", new ImmutableMap.Builder<String, Object>()
        .put("user_id", 123)
        .put("feed_slug", "timeline")
        .build());

// Our data science team will typically tell you which endpoint to use
client.personalization().get("discovery_feed", new ImmutableMap.Builder<String, Object>()
        .put("user_id", 123)
        .put("source_feed_slug", "timeline")
        .put("target_feed_slug", "user")
        .build());
// Read the personalization feed for a given user
resp, err := client.Personalization().Get("personalized_feed", map[string]interface{}{
	"user_id":   123,
	"feed_slug": "timeline",
})
if err != nil {
	// ...
}

// Our data science team will typically tell you which endpoint to use
resp, err = client.Personalization().Get("discovery_feed", map[string]interface{}{
	"user_id":          123,
	"source_feed_slug": "timeline",
	"target_feed_slug": "user",
})
if err != nil {
	// ...
}

Follow Suggestions

Stream makes it easy to add follow suggestions to your app. Simply make the API call shown below to retrieve a list of follow suggestions.

suggestions = client.personalization.get('follow_recommendations', target_feed_slug='user', user_id='123', source_feed_slug='timeline')
print(suggestions)
client.personalization().get("follow_recommendations", new ImmutableMap.Builder<String, Object>()
        .put("user_id", 123)
        .put("feed_slug", "timeline")
        .build());

Follow suggestions are disabled by default. Contact support so we can enable it for your organization.

By default the follow suggestions are based on a standardized graph analysis algorithm. Our data science team can work with you to create a customized algorithm for your app. Contact sales@getstream.io to learn more.

Analytics Clients

We recommend tracking every event for each user, which allows you to gain a better understanding of that user's interests. Common examples include:

  • Clicking on a link
  • Likes or comments
  • Sharing an activity
  • Viewing another user's profile page
  • Searching for a certain user/content/topic/etc.

Stream Analytics supports tracking events via JS, iOS, Android or email redirects:

Note: The events and data you want to track is often different than what you traditionally track in Google Analytics or Mixpanel. Stream's analytics is intended to work beside your existing analytics solution.

Analytics Installation & Setup

You're currently not logged in. Login so we can add your API key in the documentation snippets.


The code below shows how to install Stream's Analytics SDK:

// Add this async loader code anywhere in your HTML code

<script type="text/javascript">
!function(t,e){t("StreamAnalytics","https://d2j1fszo1axgmp.cloudfront.net/2.7.0/stream-analytics.min.js",e)}(function(t,e,n){var s,i,r;n["_"+t]={},n[t]=function(e){n["_"+t].clients=n["_"+t].clients||{},n["_"+t].clients[e.apiKey]=this,this._config=e};var c=function(t){return function(){return this["_"+t]=this["_"+t]||[],this["_"+t].push(arguments),this}};s=["setUser","trackImpression","trackEngagement"];for(var a=0;a<s.length;a++){var o=s[a];n[t].prototype[o]=c(o)}i=document.createElement("script"),i.async=!0,i.src=e,r=document.getElementsByTagName("script")[0],r.parentNode.insertBefore(i,r)},this);
</script>
// the client is available via CocoaPods, just add this to you Podfile

pod 'stream-analytics-ios'
// download the latest release from here: https://github.com/GetStream/stream-analytics-android/releases/

Include the above code snippet in the <head></head> section of your page. Note: the JavaScript is loaded asynchronously for optimal performance.

Using CommonJS modules

If you're using CommonJS modules, run the following command in your project directory:

npm install stream-analytics --save

After installing the package, require it in your app:

var StreamAnalytics = require('stream-analytics');

Client setup

The snippet below shows you how to initialize Stream's analytics client:

var client = new StreamAnalytics({
  apiKey: "YOUR_API_KEY",
  token: "ANALYTICS_TOKEN"
});
// Initialize StreamAnalytics in AppDelegate

#import "AppDelegate.h"
#import "Stream.h"

@interface AppDelegate ()
@end

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    //enable debug logging
    [StreamAnalytics enableLogging:YES];
    return YES;
}
@end
// Import the library in your Gradle project:

dependencies {
  compile files('/path_to_file/stream-analytics-release-1.1.0.aar')
}

// Initialize the StreamAnalytics client
StreamAnalyticsAuth auth = new StreamAnalyticsAuth("YOUR_API_KEY","ANALYTICS_TOKEN");
StreamAnalytics client = StreamAnalyticsImpl.getInstance(auth);

Add your credentials to your project's Info.plist file:


      <dict>
      <key>APIKey</key>
      <string></string>
      <key>JWTToken</key>
      <string></string>
      </dict>
    

Bind the service in your application's manifest.xml:


        <uses-permission android:name="android.permission.INTERNET" />
      

Always specify the current user before sending events:

// user id and a friendly alias for easier to read reports
client.setUser({id: 486892, alias: 'Julian'});
#import "Stream.h"


// set user id only 
[[StreamAnalytics sharedInstance] setUserId:@"486892"];

// add friendly alias for the user
[[StreamAnalytics sharedInstance] setUserId:@"486892" andAlias:@"Julian"];
// set user data
client.setUser("486892", "Eric");

// you can also set the userId only
// client.setUserId("486892");

Tracking Engagements

The snippet below shows an example of how to track engagements. Emgagement examples include likes, comments, profile views and link clicks.

var engagement = {
  // the label for the engagement, ie click, retweet etc.
  'label': 'click',
  // the ID of the content that the user clicked
  'content': {
    'foreign_id': 'tweet:34349698'
  },
  // score between 0 and 100 indicating the importance of this event 
  // IE. a like is typically a more significant indicator than a click
  'score': 2,
  // (optional) the position in a list of activities
  'position': 3,
  // (optional) the feed the user is looking at
  'feed_id': 'user:thierry',
  // (optional) the location in your app. ie email, profile page etc
  'location': 'profile_page'
};

client.trackEngagement(engagement);
StreamEngagement *event = [StreamEngagement createEngagementEvent:@"click" withContent: @{@"foreign_id":@"message:34349698"}.mutableCopy]];

event.position = @"3";
event.boost = @2;
event.feedId = @"user:thierry";
event.location = @"profile_page";

[[StreamAnalytics sharedInstance] send:event];
client.send(new Engagement.EventBuilder()
    .withFeedId("user:thierry")
    .withContent(
        new Content.ContentBuilder()
            .withForeignId("message:34349698")
            .withAttribute("verb", "share")
            .withAttribute("actor", new ContentAttribute("1", "user1"))
            .build()
        )
    .withBoost(2)
    .withLocation("profile_page")
    .withPosition("3")                
    .build()
);

Parameters

Name Type Description Optional
label string The type of event (ie. click, share, search, etc.).
content string or object The content the engagement relates to, either as an ID or content object.
score string A score between 0 and 100 indicating the importance of an event.
position string The placement in a list of activities, starting at 0.
feed_id string The specific feed the user is viewing.
location string The page in your app (ie. email, homepage, profile page, etc.).

Tracking Impressions

Tracking impressions allows you to learn what specific users are not interested in. If the app often shows posts about football, and the user never engages with those posts, we can conclude that we're displaying the wrong content. The code below shows how to track that a user viewed 3 specific activities:

var impression = {
  // the list of content IDs displayed to the user
  'content_list': ['tweet:34349698', 'tweet:34349699', 'tweet:34349697'],
  // (optional) the feed where this content is coming from
  'feed_id': 'flat:tommaso',
  // (optional) the location in your app. ie email, profile page etc
  'location': 'profile_page'
};

// send the impression events
client.trackImpression(impression);
//track an impression
StreamImpression *event = [StreamImpression createImpressionEventWithContentList:@[@"song:34349698", @"song:34349699", @"song:34349697"]];

// (optional) the feed where this content is coming from
event.feedId = @"flat:tommaso";
// (optional) the location in your app. ie email, profile page etc
event.location = @"ios-app";

// send the impression events
[[StreamAnalytics sharedInstance] send:event];
client.send(new Impression.EventBuilder()
    .withContentList(
        new Content.ContentBuilder()
            .withForeignId("tweet:34349698")
            .withAttribute("verb", "share")
            .withAttribute("actor", new ContentAttribute("1", "user1"))
            .build(),
        new Content.ContentBuilder()
            .withForeignId("tweet:34349699")
            .build(),
        new Content.ContentBuilder()
            .withForeignId("tweet:34349610")
            .build()
    )
    .withFeedId("flat:tommaso")
    .withLocation("android-app")
    .build()
);

Be sure to use the same foreign ids as used in your feeds. This allows Stream to understand the content of the activities.

Parameters

Name Type Description Optional
content_list list of strings or objects The list of content the user is looking at. Either a list of IDs or objects.
feed_id string The feed the user is looking at.
location string The location in your app (ie. email, homepage, profile page, etc.).

Email Tracking

Users tend to engage with emails, even when they aren't engaged with your app. Thus, it's important to track how they interact with your emails.

Tracking clicks in emails works via redirects. You can use our client libraries to generate a redirect link.

Note: you can only create redirect links from your server-side application using one of Stream's API clients. Refer to our API documentation for more information on how to obtain the clients.

Example

// the url to redirect to
let target_url = 'http://mysite.com/detail';

// track the impressions and a click
let impression = {
    content_list: ['tweet:1', 'tweet:2', 'tweet:3'], 
    user_data: 'tommaso', 
    location: 'email',
    feed_id: 'user:global'
};

let engagement = {
    content: 'tweet:2', 
    label: 'click',
    position: 1, 
    user_data: 'tommaso', 
    location: 'email',
    feed_id: 'user:global'
};

let events = [impression, engagement];
let tracking_url = client.client.createRedirectUrl(target_url, "tommaso", events);

// when the user opens the tracking url in their browser gets redirected to the target url the events are added to our analytics platform
# the url to redirect to
target_url = 'http://mysite.com/detail'

# track the impressions and a click
impression = {
    'content_list': ['tweet:1', 'tweet:2', 'tweet:3'], 
    'user_data': 'tommaso', 
    'location': 'email',
    'feed_id': 'user:global'
}

engagement = {
    'content': 'tweet:2', 
    'label': 'click',
    'position': 1, 
    'user_data': 'tommaso', 
    'location': 'email',
    'feed_id': 
    'user:global'
}

events = [impression, engagement]
tracking_url = client.create_redirect_url(target_url, user_id, events)

# when the user opens the tracking url in their browser gets redirected to the target url
# the events are added to our analytics platform
$client = new GetStream\Stream\Client('YOUR_API_KEY', 'API_KEY_SECRET');

// the url to redirect to
$targetUrl = 'http://my.application.com/page/';

$impression = [
  'content_list' => ['tweet:34349698', 'tweet:34349699', 'tweet:34349697'],
  'feed_id' => 'flat:tommaso',
  'location' => 'profile_page',
  'user_data' => ['id' => 'bubbles'],
  'label' => 'impression',
];

$engagement = [
    'content' => 'tweet:34349698',
    'feed_id' => 'flat:tommaso',
    'location' => 'profile_page',
    'user_data' => ['id' => 'frank'],
    'label' => 'click',
];

$events = [$impression, $engagement];
$trackingUrl = $client->createRedirectUrl($targetUrl, $events);

// when the user opens the tracking url in their browser gets redirected to the target url
// the events are added to our analytics platform
// the URL to direct to
URL targetURL = new URL("http://mysite.com/detail");

// track the impressions and a click
List<Impression> impressions = Lists.newArrayList(Impression.builder()
        .contentList(new Content("tweet:1"),
                new Content("tweet:2"),
                new Content("tweet:3"))
        .userData(new UserData("tommaso", null))
        .location("email")
        .feedID("user:global")
        .build());
List<Engagement> engagements = Lists.newArrayList(Engagement.builder()
        .content(new Content("tweet:2"))
        .label("click")
        .position(1)
        .userData(new UserData("tommaso", null))
        .location("email")
        .feedID("user:global")
        .build());

// when the user opens the tracking URL in their browser gets redirected to the target URL
// the events are added to our analytics platform
URL trackingURL = client.analytics().createRedirectURL(targetURL, impressions, engagements);
// the URL to direct to
targetURL := "http://mysite.com/detail"

// track the impressions and a click
impression := ImpressionEventsData{}.
	WithForeignIDs("tweet:1", "tweet:2", "tweet:3").
	WithUserData(NewUserData().String("tommaso")).
	WithLocation("email").
	WithFeedID("user:global")

engagement := EngagementEvent{}.
	WithForeignID("tweet:2").
	WithLabel("click").
	WithPosition(1).
	WithUserData(NewUserData().String("tommaso")).
	WithLocation("email").
	WithFeedID("user:global")

trackingURL, err := client.Analytics().RedirectAndTrack(targetURL, impression, engagement)
if err != nil {
	panic(err)
}
// when the user opens the tracking URL in their browser gets redirected to the target URL
// the events are added to our analytics platform

In the code above, when a user clicks the tracking URL, they are re-directed to the specified target URL. During the re-direct, Stream tracks the impressions and engagement events you specified.

Enrichment

One of the most important best-practices when integrating with Stream is to keep your data normalized and use references to objects instead full objects inside activities. The What To Store section covers this topic.

In most cases it is better to store a reference inside your activities instead of an entire objects. Doing so will make it easier to update data and it will avoid complex sync problems. Because of this a correct integration with Stream stores references to objects when adding activities and replaces them with full objects when reading the feeds. For example: if your activities have users as actors, your application will store the user ID in the actor field and replace the ID value with the user object before rendering the feed.

The process of replacing objects to refs and refs to objects is called "Enrichement" and can be done in two different ways.

Enrichment using Stream Collection and User Endpoints

The simplest way to organize your data is to store objects and user on Stream using collections and users API endpoints and then embed them inside your activities. With this method, you don't have to implement any server-side logic to convert objects into references and references back into full objects. Activities read from the APIs will be fully enriched with your data.

Another advantage is that you can now read feeds and add activities directly from your frontend or mobile app.

Enrichment using your own database (backend)

If you only talk to Stream from the backend and you must keep data on your servers, the best approach is to use your own database for the enrichment process. Both the APIs and the API clients will make this very easy.

To clarify, we want to translate this:

{
    actor: 'User:1',
    verb: 'pin', 
    object: 'Place:42'
};
{
    "actor": "User:1",
    "verb": "pin", 
    "object": "Place:42"
}

Into this (UserObject, PlaceObject and BoardObject are the objects stored in your database) :

{
    actor: UserObject,
    verb: 'pin', 
    object: PlaceObject
};
{
    "actor": UserObject,
    "verb": "pin", 
    "object": PlaceObject
}

Stream provides enrichment out-of-the-box for some of the most popular ORMs. Please find below the examples for how to enrich activities using different combinations of frameworks/ORMs

 // https://github.com/GetStream/stream-node-orm
var streamNode = require('getstream-node');
var streamWaterline = new streamNode.WaterlineBackend()
streamWaterline.enrichActivities(activities).then(function(enrichedActivities) {
    res.json({'results': enrichedActivities})
}).catch(function(err) {
    sails.log.error('enrichment failed', err)
    return res.serverError('failed to load articles in the feed')
})
 // https://github.com/GetStream/stream-node-orm
var streamNode = require('getstream-node');
var streamMongoose = new streamNode.MongooseBackend()
streamMongoose.enrichActivities(activities).then(function(enrichedActivities) {
    console.log(enrichedActivities)
}).catch(function(err) {
    console.log('error', err)
})
 # https://github.com/GetStream/stream-django
from stream_django.enrich import Enrich
enricher = Enrich()
enriched_activities = enricher.enrich_activities(activities)
 # https://github.com/GetStream/stream-rails
enricher = StreamRails::Enrich.new
enriched_activities = enricher.enrich_activities(activities)
 # https://github.com/GetStream/stream-laravel
use GetStream\StreamLaravel\Enrich;
$enricher = new Enrich();
$activities = $feed->getActivities(0,25)['results'];
$activities = $enricher->enrichActivities($activities);

Feel free to reach out if your favorite ORM or Framework is missing.

Collections and Users

Stream allows you to store arbitrary data with the collections and users API endpoints. Data stored this way can also be embedded inside activities so that enrichment is done automatically on Stream's side.

Here is a few examples on how you can embed a user and an object inside an activity.

let feed = client.feed("user", "jack");
await client.setUser({name: 'Jack'});

let post = await client.collections.add("post", "42-ways-to-improve-your-feed", {text:"..."});

await userFeed.addActivity({
  actor: client.currentUser,
  verb: "post",
  object: post,
})

// if we now read Jack's feed we will get automatically the enriched data
let response = await userFeed.get({enrich: true});
console.log(response);

// we can also update Jack's post and get the new version automatically propagated to his feed and its followers
await client.collections.update("post", "42-ways-to-improve-your-feed", {text:"new version of the post"});

// jack's feed now has the new version of the data
let response = await userFeed.get({enrich: true});
feed = client.feed("user", "jack")

# make sure we have the user data on Stream
user = client.users.add("jack", {"name": "Jack", "profile_picture": "https://goo.gl/XSLLTA"}, get_or_create=True)

# then create a post inside a collection
item = client.collections.add("post", "42-ways-to-improve-your-feed", {text:"..."})

# add the activity with user and post objects references
feed.add_activity({
    "actor": client.users.create_reference(user),
    "verb": "post",
    "object": client.collections.create_reference(item),
})

# if we now read Jack's feed we will get automatically the enriched data
feed.get(enrich=True)

# we can also update Jack's post and get the new version automatically propagated to his feed and its followers 
client.collections.update("items", item["id"], data={"text":"new version of the post"})
FlatFeed feed = client.flatFeed("user", "jack")

// make sure we have the user data on Stream
User user = client.user("jack").getOrCreate(new Data()
        .set("name", "Jack")
        .set("profile_picture", "https://goo.gl/XSLLTA"));

// then create a post inside a collection
CollectionData item = client.collections().add("post", new CollectionData("42-ways-to-improve-your-feed")
        .set("text", "...")).join();

// add the activity with user and post objects references
userFeed.addActivity(Activity.builder()
        .actor(createUserReference(user.getID()))
        .verb("post")
        .object(createCollectionReference(item.getCollection(), item.getID()))
        .build()).join();

// if we now read Jack's feed we will get automatically the enriched data
feed.getEnrichedActivities();

// we can also update Jack's post and get the new version automatically propagated to his feed and its followers
client.collections().update(item.getCollection(), item.set("text", "new version of the post")).join();
feed := client.FlatFeed("user", "jack")

// make sure we have the user data on Stream
userData := stream.User{
	ID: "jack",
	Data: map[string]interface{}{
		"name":            "Jack",
		"profile_picture": "https://goo.gl/XSLLTA",
	},
}
user, err := client.Users().Add(userData, true)

// then create a post inside a collection
collectionData := stream.CollectionObject{
	ID: "42-ways-to-improve-your-feed",
	Data: map[string]interface{}{
		"text": "...",
	},
}
item, err := client.Collections().Add("post", collectionData)

// add the activity with user and post objects references
activity := stream.Activity{
	Actor:  client.Users().CreateReference(user.ID),
	Verb:   "post",
	Object: client.Collections().CreateReference("post", item.ID),
}
feed.AddActivity(activity)

// if we now read Jack's feed we will get automatically the enriched data
enrichedResponse, err := feed.GetEnrichedActivities()

// we can also update Jack's post and get the new version automatically propagated to his feed and its followers
client.Collections().Update("items", item.ID, map[string]interface{}{"text": "new version of the post"})
var feed = client.Feed("user", "jack");

// make sure we have the user data on Stream
var userData = new Dictionary<string, object>()
{
        { "name", "Jack"},
        {"profile_picture", "https://goo.gl/XSLLTA"}
};
var user = await client.Users.Add("jack", userData, true);

// then create a post inside a collection
var collectionData = new GenericData();
collectionData.SetData("text", "...");
var item = await client.Collections.Add("post", collectionData, "42-ways-to-improve-your-feed");

// add the activity with user and post objects references
var activity = new Activity(user.Ref(), "post", item.Ref("post"));
await feed.AddActivity(activity);

// if we now read Jack's feed we will get automatically the enriched data
await feed.GetEnrichedFlatActivities();

// we can also update Jack's post and get the new version automatically propagated to his feed and its followers client.collections.update("items", item["id"], data={text:"new version of the post"})

More on how to work with references to collections and users can be found here.

Files and Images

This API endpoint allows you to upload files and to process your images (eg. create image thumbnails).

Upload

Image and Files have separate API endpoints (eg. images can be resized, files cannot). Once the upload is completed the URL of the file/image is returned and is ready for use.

The returned URL is served via CDN and can be requested by anyone; in order to avoid resource enumeration attacks, a unique signature is added. Manipulating the returned URL will likely result in HTTP errors.

// uploading an image from the browser (input is a <input type="file"> form field)
client.images.upload(input.files[0]);

// uploading an image from React Native (imageURI is the URI of the file)
client.images.upload(imageURI);

// uploading a file from the browser (input is a <input type="file"> form field)
client.files.upload(input.files[0]);

// uploading a file from React Native (fileURI is the URI of the file)
client.files.upload(fileURI);
Note: upload size is limited to 10MB, any attempt to upload a larger file will return an HTTP 413 error. Please contact support if you need to upload larger files.

Delete

Files and images can be deleted using their URL.

// deleting an image using the url returned by the APIs
client.images.delete(imageURL);

// deleting a file using the url returned by the APIs
client.files.delete(fileURL);
Note: file/image will still be available on the CDN until their cache is expired.

Process images

Once an image is uploaded, it is possible to create variants of the same in different sizes.
The image to manipulate is selected using its URL. A new URL is then returned by the API.

Parameters

Name Type Description Default
resize string the strategy used to adapt the image the new dimensions clip
crop string the cropping mode center
w number the final width of the processed image
h number the final height of the processed image

Resize

The resize parameter determines how the new image dimensions are applied. The default value for this parameter is "clip".

Allowed values:
  • clip fits the image within the width and height boundaries without cropping or changing the aspect ratio.
  • crop applies the new dimensions and keeps the aspect ratio by cropping the parts of the image outside of the new boundaries. You can use the crop parameter to control how the cropping is done.
  • scale applies the new dimensions to the image. This method does not respect the aspect ratio and can create distorted images.
  • fill same as "clip" but the resulting image will have the exact dimensions requested and filled with a solid background color.

Crop

When resizing the image with a crop, you can specify the cropping direction. By default images are copped started from their center. You combine directions in order to pick corners. Eg. "top,left" will use the top left corner as cropping position.

Allowed values: top, bottom, left, right, center

// create a 50x50 thumbnail and crop from center
client.images.process(imageURL, {h:50, w:50, resize:'crop'});

// create a 50x50 thumbnail using clipping (keeps aspect ratio)
client.images.process(imageURL, {h:50, w:50, resize:'clip'});

Scrape Open Graph Metadata from URLs

This endpoint allows you to retrieve open graph information from a URL which you can use to add images and a description to activities.

let urlPreview = await client.og('http://www.imdb.com/title/tt0117500/');

Returns the following:

{ "url":"http://www.imdb.com/title/tt0117500/", "images":[ { "image":"https://m.media-amazon.com/images/M/MV5BZDJjOTE0N2EtMmRlZS00NzU0LWE0ZWQtM2Q3MWMxNjcwZjBhXkEyXkFqcGdeQXVyNDk3NzU2MTQ@._V1_UY1200_CR90,0,630,1200_AL_.jpg" } ], "type":"video.movie", "title":"The Rock (1996)", "site_name":"IMDb", "description":"Directed by Michael Bay. With Sean Connery, Nicolas Cage, Ed Harris, John Spencer. A mild-mannered chemist and an ex-con must lead the counterstrike when a rogue group of military men, led by a renegade general, threaten a nerve gas attack from Alcatraz against San Francisco." }

Social Music App tutorial

This tutorial explains how to use Stream to build a scalable social network for music. In this example app you can follow your friends, artists and playlists. We're going to support the following features:

  • Timeline/Newsfeed
  • Hashtags
  • @mentions
  • Likes & comments
  • Notification feed
  • Realtime changes

Step 1: Setting up the feed groups

First of all we need to create feed groups for everything you can follow. For this app that will be "user", "artist" and "playlist". Next we'll need feeds to display the data. So we'll create the "timeline", "notification" and "tag" feeds groups. Take a moment to go to the dashboard and create these 6 feed groups. The "notification" feed group should be the "notification" feed type. The other 5 feed groups can be of the "flat" type.

Step 2: Follow users, artists and playlists

The example below shows you how to follow users, artists and playlists.

import stream

# Instantiate a new client
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', 'APP_ID')
timeline_chris = client.feed('timeline', 'chris')

# Follow a friend
timeline_chris.follow('user', 'jack')
# Follow a playlist
timeline_chris.follow('playlist', '90s_hits')
# Follow an artist
timeline_chris.follow('artist', 'coldplay')
// Instantiate a new client
client, err := stream.NewClient(
    // API key
    "YOUR_API_KEY",
    // API secret
    "API_KEY_SECRET",
    // API region
    stream.WithAPIRegion("us-east"),
)
if err != nil {
    panic(err)
}

timelineChris := client.FlatFeed("timeline", "chris")
// follow a friend
userJack := client.FlatFeed("user", "jack")
err = timelineChris.Follow(userJack)
if err != nil {
    panic(err)
}
// follow a playlist
playlist90s := client.FlatFeed("timeline", "90s_hits")
err = timelineChris.Follow(playlist90s)
if err != nil {
    panic(err)
}
// follow an artist
artistColdplay := client.FlatFeed("artist", "coldplay")
err = timelineChris.Follow(artistColdplay)
if err != nil {
    panic(err)
}
// Instantiate a new client
var client = new StreamClient("YOUR_API_KEY", "API_KEY_SECRET");
var timelineChris = client.Feed("timeline", "chris");

// Follow a friend
await timelineChris.FollowFeed("user", "jack");
// Follow a playlist
await timelineChris.FollowFeed("playlist", "90s_hits");
// Follow an artist
await timelineChris.FollowFeed("artist", "coldplay");

Step 3: Posting an update

Let's dive straight into an example of how to add an activity:

jack = client.feed("user", "jack")

# Jack posts an update with a hashtag and @mention
jack.add_activity({
  "actor": "jack",
  "verb": "post",
  "object': "post:10",
  "foreign_id": "post:10",
  "to": ["notification:chris", "tag:amsterdam"],
  "message": "Hi @chris, the coldplay concert was totally amazing #amsterdam"
})
// Jack posts an update with a hashtag and @mention
_, err = userJack.AddActivity(stream.Activity{
    Actor:     "jack",
    Verb:      "post",
    Object:    "post:10",
    ForeignID: "post:10",
    To: []string{
        "notification:chris",
        "tag:amsterdam",
    },
    Extra: map[string]interface{}{
        "message": "Hi @chris, the coldplay concert was totally amazing #amsterdam",
    },
})
if err != nil {
    panic(err)
}
var jack = client.Feed("user", "jack");

// Jack posts an update with a hashtag and @mention
var activity = new Activity("jack", "post", "post:10")
{
    ForeignId = "post:10",
    To = new string[] { "notification:chris", "tag:amsterdam" }
};
activity.SetData("message", "Hi @chris, the coldplay concert was totally amazing #amsterdam");
await jack.AddActivity(activity);

First of all note how easy it is to add an activity. Certain fields like actor, verb and object are mandatory. Message is a custom field. You can add as many custom fields as you like. You can store numbers, strings, lists and even objects.

The TO field is comparable to how CC works for email. For a production app we recommnend storing the activity in your database, and syncing it to Stream.

Step 4: Likes & Comments

When a user likes an activity or writes a comment you'll want to:

  • Notify the author of the original activity
  • Show the like/comments to followers

The example below uses the TO support to write both to the user:chris and notification:jack feeds:

# Chris likes Jack's post
chris = client.feed("user", "chris")
chris.add_activity({
  "actor": "chris",
  "verb": "like",
  "object": "post:10",
  "foreign_id": "like:25",
  "to": ["notification:jack"]
})
# Notify Jack and show it to Chris' followers (via the "chris" user feed)
// Chris likes Jack's post
userChris := client.FlatFeed("user", "chris")
_, err = userChris.AddActivity(stream.Activity{
    Actor:     "chris",
    Verb:      "like",
    Object:    "post:10",
    ForeignID: "like:25",
    To: []string{
        "notification:jack",
    },
})
if err != nil {
    panic(err)
}
// Notify Jack and show it to Chris' followers (via the chris' user feed)
// Chris likes Jack's post
var chris = client.Feed("user", "chris");
var chrisActivity = new Activity("chris", "like", "post:10")
{
    ForeignId = "like:25",
    To = { "notification:jack" }
};
await chris.AddActivity(chrisActivity);

// Notify Jack and show it to Chris' followers (via the "chris" user feed)

Step 5: Reading a feed

It's easy to read a feed. You can use either offset or id_lt based pagination. For larger apps we recommend filtering using id_lt as it performs much better.

jack = client.feed('timeline', 'jack')

# Read the timeline for jack
activities = jack.get(limit=10)['results']
# Read the next page, use id filtering for optimal performance
next_activities = jack.get(limit=10, id_lt=activities[-1]['id'])['results']

# Reading a notification feed is very similar
chris = client.feed('notification', 'chris')
notifications = chris.get(limit=10)['results']
// read the timeline for jack
timelineJack := client.FlatFeed("timeline", "jack")
resp, err := timelineJack.GetActivities(stream.WithActivitiesLimit(10))
if err != nil {
    panic(err)
}
for _, activity := range resp.Results {
    // ...
}
// read the next page, use id filtering for optimal performance
nextResp, err := timelineJack.GetNextPageActivities(resp)
if err != nil {
    panic(err)
}
for _, activity := range nextResp.Results {
    // ...
}
// reading a notification feed is very similar
notificationChris := client.NotificationFeed("notification", "chris")
notificationResp, err := notificationChris.GetActivities(stream.WithActivitiesLimit(10))
if err != nil {
    panic(err)
}
for _, result := range notificationResp.Results {
    for _, activity := range result.Activities {
        // ...
    }
}
var jackTimeline = client.Feed("timeline", "jack");

// Read the timeline for jack
var activities = await jackTimeline.GetActivities(0, 10);
// Read the next page, use id filtering for optimal performance
var nextActivities = await jack.GetActivities(0, 10, FeedFilter.Where().IdLessThan(activities.Last().Id));

// Reading a notification feed is very similar
chris = client.Feed("notification", "chris");
var notifications = await chris.GetActivities(0, 10);

The notification feed also includes the count of unseen and unread activities.

Step 6: Realtime changes

Stream allows you to listen to feed changes in realtime. You can use either websockets, SQS queues or Webhooks to receive notifications.

The following code creates a web socket connection to receive feed updates:
<script type="text/javascript">
    function failCallback(data) {
        alert('something went wrong, check the console logs');
        console.log(data);
    }

    user1 = client.feed('user', '1', token);
    var subscription = user1.subscribe(function callback(data) {
        alert(data);
    }).then(null, failCallback);

    // The returned Subscription object has a method cancel
    // that will remove the listener from this channel 
</script>

Notification System Tutorial

This tutorial explains how easy it is to build a notification feed with Stream.

Step 1: Initialize your notification system

To start, we'll describe how to notify 'Scott' that his colleague 'Josh' started following him. The code shows you how to connect to Stream and add an activity with the verb "follow". (Note: You can lookup your API key and secret in the dashboard.)

# pip install stream-python
import stream
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET', location='us-east')
# add an activity to Scott's notification feed
notification_feed_scott = client.feed('notification', 'scott')
activity_data = {'actor': 'Josh', 'verb': 'follow', 'object': 'Scott', 'time': '2018-05-04T20:24:03.042927'}
activity_response = notification_feed_scott.add_activity(activity_data)

Step 2: Reading the notification feed

Now, read Scott's notification feed to see how it looks:

# Read Scott's notification feed
scott = client.feed('notification', 'scott')
notifications = scott.get(limit=10)
print(notifications)

Now, bring your attention to the following details within the response: the notification counts and the group. You will see the notification counts in the response: 'unread': 1 and 'unseen': 1. Many notification systems will differentiate between a notification that has been seen as opposed to a notification that has been marked read. The design below highlights the common features of a notification feed:

Notice that each notification contains a list of activities. The activities are grouped together based on the rule set and defined within your Stream dashboard. Note: the group name of follow_2018-05-04 currently contains 1 activity. By default, aggregation groups are based on the activity's verb and time. For example, if multiple people follow you on the same day, there will only be 1 notification. This allows you to show a notification like: "Josh and 3 others followed you". Aggregation helps reduce noise within the notification feed, which is especially important when there's an abundance of user engagement. You can configure or disable the exact rules for how activites are grouped together in the dashboard.

Step 3: Realtime notification system

Stream includes support for listening to changes to your notification feed in real-time. You can listen to changes using Websockets, SQS or webhooks. The example below shows how to use websockets to update the feed in real time. The first step is to generate a token for authorizing the client-side connection for reading the feed.

token = client.feed('notification', 'scott').get_readonly_token()
print(token)

Next, use the token to connect to Stream's websocket infrastructure. Create a file called test.html and paste the following code (remember to use the token you generated above):

<html>
<script src="https://cdn.jsdelivr.net/npm/getstream/dist/js_min/getstream.js"></script>
<script type="text/javascript">
    var token = 'your-token-here';
    var client = stream.connect('YOUR_API_KEY', null, 'SITE_ID');
    var notificationScott = client.feed('notification', 'scott', token);

    function callback(data) {
        alert('A new activity: ' + JSON.stringify(data));
    }

    function successCallback() {
        alert('Now listening to changes in realtime. Add an activity to see how realtime works.');
    }

    function failCallback(data) {
        alert('Something went wrong, check the console logs');
        console.log(data);
    }

    notificationScott.subscribe(callback).then(successCallback, failCallback);
</script>
</html>

Step 4: Follow relationships

At this point, you should have a good understanding of how to add content directly to a user's notification feed. However, there are many use cases for which you may want to notify a group of users. Some examples include:

  • Site wide announcements to all users.
  • New lectures or homework assignments for a class of students.
  • Tickets going on sale for your favorite artist.
  • A new event for the group/meetup you follow.

For these types of use cases we recommend creating follow relationships. Let's use the meetup scenario as an example. The code below shows how to follow the golang meetup. It also shows how to create a new event activity for the meetup. Now, you've probably noticed the "meetup" feed group isn't configured at this point. To do so, head over to the dashboard and configure a flat feed group called "meetup".

client.feed('notification', 'scott').follow('meetup', 'golang')
# Scott will now receive notifications about new activities added to the golang meetup feed
activity_data = {'actor': 'Jimmy', 'verb': 'add', 'object': 'event:123', 'event': {'name': 'golang package manager changes', 'location': 'Boulder'}, 'time': '2018-05-04T20:24:03.042927'}
activity_response = client.feed('meetup', 'golang').add_activity(activity_data)

Step 5: Read the feed again

To see what has changed, take a look at the feed. You'll notice the event announcement is now present in Scott's feed. In addition, the activities were not grouped together. The event announcement is in group 'add_2018-05-04' and the follow notification is in group 'follow_2018-05-04'. The follow relationship is very convenient for when many people need to be notified.

# Read Scott's notification feed and mark the notifications as seen
notifications = client.feed('notification', 'scott').get(limit=10, mark_seen=True)
print(notifications)

Step 6: Marking notifications as seen

Did you notice how the request above marked the notification as seen? Note how reading the feed and marking the items as seen is combined in 1 API call. This is convenient since most notification feeds mark items as seen the same moment you open the feed.

When you read the feed again you'll see the notifications changed to 'seen'. You can also mark notifications as 'read'. This is typically done when a user clicks on a notification. The example below shows this process in action:

# Mark an activity as read
notifications = client.feed('notification', 'scott').get(mark_read=["insert-your-notification-id-here"])
# The notification id looks like this: 16014de8-4fd9-11e8-8080-800162bf0445.add_2018-05-04

Step 7: React, JS, Android, iOS and React Native notification systems

We are working on a set of libraries to simplify getting started with frontend development for notification feeds. Shoot us an email describing what you're building and, if available, we'll share an early release.

Concluding the notification system tutorial

We hope you enjoyed this tutorial for building notification systems. If you have any questions please contact us.