Scalable news feeds & activity streams with Golang

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 --save

// install using bower
bower install getstream

// latest build is available at this link as well
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-repo-apache</artifactId>
    <version>1.2.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 (client side)
client = stream.connect('YOUR_API_KEY', null, 'APP_ID');
// Find your API keys here https://getstream.io/dashboard/
# 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.config.ClientConfiguration;
// import io.getstream.client.model.feeds.Feed;

ClientConfiguration streamConfig = new ClientConfiguration();
StreamClient streamClient = new StreamClientImpl(streamConfig, "YOUR_API_KEY", "API_KEY_SECRET");
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");
Note: Stream's API uses SSL to keep your data secure. SSL can give errors if your libraries are outdated. See this discussion.

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.

Promises are available since version 3.0.0 of the Stream API client

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);
Feed chrisUserFeed = streamClient.newFeed("user", "chris");

// Create an Activity service
FlatActivityServiceImpl<SimpleActivity> flatActivityService = chrisUserFeed.newFlatActivityService(SimpleActivity.class);

// Add an Activity; message is a custom field - tip: you can add unlimited custom fields!
SimpleActivity activity = new SimpleActivity();
activity.setActor("chris");
activity.setObject("picture:10");
activity.setVerb("add");
SimpleActivity response = flatActivityService.addActivity(activity);

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

// Read Jack's timeline and Chris' post appears in the feed:
List<SimpleActivity> jackTimeline = jackTimeline.newFlatActivityService(SimpleActivity.class).getActivities(new FeedFilter.Builder()).getResults();

# Remove an Activity by referencing it's foreign_id
chrisUserFeed.deleteActivityByForeignId("picture:10");
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, an object, and a target. 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
  • Target (Optional)
  • 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)
  • Target: "Places to Visit" (Board:1)
  • 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", :target => "Board:1"}
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', target: 'Board:1'};

// 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", "target": "Board:1"}
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",
    "target"=>"Board:1",
];

$userFeed1->addActivity($data);
// Instantiate a feed object
Feed feed = streamClient.newFeed("user", "1");

// Create an activity service
FlatActivityServiceImpl<SimpleActivity> flatActivityService = feed.newFlatActivityService(SimpleActivity.class);

// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
SimpleActivity activity = new SimpleActivity();
activity.setActor("User:1");
activity.setObject("Place:42");
activity.setVerb("pin");
activity.setTarget("Board:1");
SimpleActivity response = flatActivityService.addActivity(activity);
// 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",
    Target: "Board:1",
}
_, 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
target string The optional target 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);
// You can add custom data to your activities.
//
// Here's an example on how to do it:
// https://github.com/GetStream/stream-java/blob/master/stream-repo-apache/src/test/java/io/getstream/client/apache/example/mixtype/MixedType.java
// 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"
}

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
feed.deleteActivity("e561de8f-00f1-11e4-b400-0cc47a024be0");

// Remove activities by their foreign_id
feed.deleteActivityByForeignId("run:1");
// 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);
SimpleActivity activity = new SimpleActivity();
activity.setActor("actor");
activity.setObject("object");
activity.setTarget("target");
activity.setTime(new Date());
activity.setForeignId("foreign1");
activity.setVerb("verb");

StreamActivitiesResponse<SimpleActivity> response = flatActivityService.updateActivities(Collections.singletonList(activity));

response.getActivities(); // list of activities
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(
  foreignID='product:123',
  time='2016-11-10T13:20:00.000000',
  set={ ... },
  unset=[ ... ],
)
# Not supported yet
// Not supported yet
// 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
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

Retrieving Activities

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

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

# Get 5 activities with id less than the given UUID (Faster - Recommended!)
result = user_feed_1.get(:limit=>5, :id_lt=>'e561de8f-00f1-11e4-b400-0cc47a024be0')

# Get activities sorted by rank (Ranked Feeds Enabled):
result = user_feed_1.get(:limit=>5, :ranking=>'popularity')
/*
 * Get 5 activities with id less than the given UUID (Faster - Recommended!)
 */
user1.get({ limit:5, id_lt: 'e561de8f-00f1-11e4-b400-0cc47a024be0' })
  .then(activitiesSuccess)
  .catch(activitiesError);

/*
 * Get activities from 5 to 10 (Pagination-Based - Slower - Less Recommended)
 */
user1.get({ limit:5, offset:5 })
  .then(activitiesSuccess)
  .catch(activitiesError);

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



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

function activitiesError(errorData) {
  console.log(errorData);
}
# Get 5 activities with id less than the given UUID (Faster - Recommended!)
result = user_feed_1.get(limit=5, id_lt="e561de8f-00f1-11e4-b400-0cc47a024be0")

# Get activities from 5 to 10 (Pagination-based - Slower)
result = user_feed_1.get(limit=5, offset=5)

# Get activities sorted by rank (Ranked Feeds Enabled):
result = user_feed_1.get(limit=5, ranking="popularity")
// Get activities from 5 to 10 (limit and offset pagination - slower)
$results = $userFeed1->getActivities(5, 5);

// Get 5 activities with id less than the given UUID (Faster - Recommended!)
$options = ['id_lte' => $last_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 activities from 5 to 10 (using offset pagination)
FeedFilter filter = new FeedFilter.Builder().withLimit(5).withOffset(5).build();
List<SimpleActivity> activities = flatActivityService.getActivities(filter).getResults();

// Filter on an id less than the given UUID
aid = "e561de8f-00f1-11e4-b400-0cc47a024be0";
FeedFilter filter = new FeedFilter.Builder().withIdLowerThan(aid).withLimit(5).build();
List<SimpleActivity> activities = flatActivityService.getActivities(filter).getResults();
// Get 5 activities with id less than the given UUID (Faster - Recommended!)
resp, err := userFeed.GetActivities(
    stream.WithActivitiesLimit(5),
    stream.WithActivitiesIDLT("e561de8f-00f1-11e4-b400-0cc47a024be0"),
)
if err != nil {
    panic(err)
}
// Get activities from 5 to 10 (Pagination-based - Slower)
resp, err = userFeed.GetActivities(
    stream.WithActivitiesLimit(5),
    stream.WithActivitiesOffset(5),
)
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 5 activities with id less than the given UUID (Faster - Recommended!)
var result = await userFeed1.GetActivities(0, 5, FeedFilter.Where().IdLessThan("e561de8f-00f1-11e4-b400-0cc47a024be0"));

// Get activities from 5 to 10 (Pagination-based - Slower)
var result = await userFeed1.GetActivities(5, 5);

// 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

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.

Passing both id_lt[e] and id_gt[e] is not supported.

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.

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
Feed timeline_feed_1 = streamClient.newFeed("timeline", "timeline_feed_1");
timeline_feed_1.follow("user", "user_42");
// 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_42
timeline_feed_1.unfollow("user", "user_42");

// Stop following feed user_42 keeping the history
timeline_feed_1.unfollow("user", "user_42", true);
// 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);
// Retrieve first 10 followers of a feed
FeedFilter filter = new FeedFilter.Builder().withLimit(10).build();
List<FeedFollow> followersPaged = feed.getFollowers(filter);
// 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 first 10 followers of a feed
FeedFilter filter = new FeedFilter.Builder().withLimit(10).build();
List<FeedFollow> followingPaged = feed.getFollowing(filter);
// 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)
  • target the activity target (e.g. Place:9)
  • 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 target, verb id and day:
    {{ target }}_{{ verb }}_{{ time.strftime("%Y-%m-%d") }}
  • Follows by actor, likes by target
    {% if verb == 'follow' %}{{ actor }}{% else %}{{ target }}{% endif %}_{{ verb }}_{{ time.strftime("%Y-%m-%d") }}

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.

Example data

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.


          The example "aggregation_feed_retrieve_example_data" couldn't be loaded
 	  		

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") }}

Example data

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);
// Mark all activities in the feed as seen and read
NotificationActivityServiceImpl<SimpleActivity> notificationActivityService = feed.newNotificationActivityService(SimpleActivity.class);
notificationActivityService.getActivities(new FeedFilter.Builder().build(), true, true);

// Mark one activity as read by its id
MarkedActivity marker = new MarkedActivity.Builder().withActivityId("e561de8f-00f1-11e4-b400-0cc47a024be0").build();
notificationActivityService.getActivities(new FeedFilter.Builder().build(), marker, new MarkedActivity.Builder().build());
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.

Realtime updates

Stream allows you to listen to feed changes in real-time. As a first step we need to generate a token to give your client side Javascript access to the feed. By default feed tokens will give both read and write permissions. You can also generate read only tokens.

# read + write token for feed user:1
token = client.feed('user', '1').token
# Generating tokens for client side usage
readonly_token = client.feed('user', '1').readonly_token
// read + write token for feed user:1
var token = client.feed('user', '1').token;
// creates a readonly token for feed user:1
var readonlyToken = client.feed('user', '1').getReadOnlyToken();
# read + write token for feed user:1
token = user_feed_1.token
# readonly token
readonly_token = user_feed_1.get_readonly_token()
// read + write token for feed user:1
$token = $userFeed1->getToken();

// creates a read-only token for feed user:1
$readonlyToken = $userFeed1->getReadonlyToken();
// read and write token for feed user:1
String token = userFeed1.getToken();

// readonly token
String readOnlyToken = userFeed1.getReadOnlyToken();
// read + write token for feed user:1
token := userFeed.RealtimeToken(false)
// readonly token
readonlyToken := userFeed.RealtimeToken(true)

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.

Note: To receive real-time notifications for a feed, you need to enable the feature via the Dashboard's feed configuration page.

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

<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', null, 'SITE_ID');
    var user1 = client.feed('user', '1', '{{ user_feed_1.token }}');

    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);
</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":"",
  }
  

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.

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);
// 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)
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!

Personalization & Analytics

Personalization

Stream makes it easy to add personalized feeds to your application. You can setup personalized feeds by implementing our analytics platform and reaching out to our data science and sales team.

As your users interact with your application, Stream starts to understand what they are interested in. You can use these insights to do a whole lot of awesome:

  • Personalize Feeds
  • Create Follow Suggestions
  • Optimize Emails
  • Product Recommendations
  • Content Recommendations

This is our most advanced feed technology to date. It enables you to substantially improve the engagement, conversion and retention within your app.

Learn more about Personalization.

Analytics

The Stream Analytics platform helps you understand how your users are engaging with your feed.

Quickly glean insights on:

  • Feed Quality
  • Users & Interests
  • Content Producers

Analytics provides specialized reporting that goes far beyond pageviews - feed-specific insights you won't get from GA or Mixpanel.

Learn more about Analytics.

In-Depth Documentation

Documentation for Personalization & Analytics.

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

SimpleActivity activity = new SimpleActivity();
activity.setActor("user:Eric");
activity.setObject("tweet:id");
activity.setVerb("tweet");
activity.setForeignId("tweet:1");
activity.setTo(Arrays.asList("notification:Jessica"));
SimpleActivity response = flatActivityService.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
// Create a new activity
SimpleActivity activity = new SimpleActivity();
activity.setActor("Player:Suarez");
activity.setObject("Player:Ramos");
activity.setVerb("foul");
SimpleActivity response = flatActivityService.addActivity(activity);
// 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 countsenric 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 reading the references in the feed and looking up the corresponding objects is called enrichment. The next section explains how to do that efficiently.

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 Collections (frontend)

When your app or website talks to stream directly you should use the enrichment using Stream Collections. In this case stream will automatically replace references in your activities with data that is stored in the Collections. More information can be found in the frontend section of the docs.

Enrichment using your own database (backend)

If you only talk to Stream from the backend, 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',
    target: 'Board:1'
};
{
    "actor": "User:1",
    "verb": "pin", 
    "object": "Place:42",
    "target": "Board:1"
}

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

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

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.

Geolocation support

Stream's support for Geolocations is currently in BETA. You can send a GEO point on any custom field. Have a look at the location field specified below:

# 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);
// You can add custom data to your activities.
//
// Here's an example on how to do it:
// https://github.com/GetStream/stream-java/blob/master/stream-repo-apache/src/test/java/io/getstream/client/apache/example/mixtype/MixedType.java
// 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);

The location field is just an example. You can pass a point value on any custom field. This feature is currently in BETA and we're working on expanding it. We'd love to hear what you're building and how we can help. Please contact support about your use case.

iOS & Android

Many of our users build native iOS and Android apps. We recommend that you integrate with Stream from your server side codebase. It's not recommended to integrate directly from the browser, your iOS or Android app. The reason for this is that security and data enrichment are hard to achieve client side.

We have prepared an example Photo Sharing application to see our best practices when building a mobile application with Stream. The backend is written in Go and manages all communication with Stream, and then communicates with the example Android application.
Go backend: https://github.com/GetStream/Stream-Example-Go-PhotoShare-Service
Android example: https://github.com/GetStream/Stream-Example-Android

Real-Time Updates

The exception to this recommendation is listening to real-time updates.

As discussed in the documentation, real-time updates should be handled client side. Feel free to get in touch if you're running into trouble.

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 */
FollowMany followMany = new FollowMany.Builder()
    .add("user:1", "user:2")
    .add("user:1", "user:3")
    .add("user:1", "user:4")
    .add("user:2", "user:3")
    .build();
feed.followMany(followMany);
// 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);
FlatActivityServiceImpl<SimpleActivity> flatActivityService = feed.newFlatActivityService(SimpleActivity.class);

/* activity 1 */
SimpleActivity activity = new SimpleActivity();
activity.setActor("actor");
activity.setObject("object");
activity.setTarget("target");
activity.setVerb("verb");
activity.setForeignId("foreign1");

/* activity 2 */
SimpleActivity activity2 = new SimpleActivity();
activity2.setActor("actor");
activity2.setObject("object");
activity2.setTarget("target");
activity2.setVerb("verb");
activity2.setForeignId("foreign2");

List<SimpleActivity> listToAdd = new ArrayList<>();
listToAdd.add(activity);
listToAdd.add(activity2);

StreamActivitiesResponse<SimpleActivity> streamResponse = flatActivityService.addActivities(listToAdd);
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);
/* Batch adding activities to many feeds */
flatActivityService.addActivityToMany(ImmutableList.<String>of("user:1", "user:2").asList(), myActivity);
// 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
// not supported yet
// 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 either follow or add_activities
feedId string The feed to import data to, for example: user:bob
data array The data for that particular instruction. See below

The data field

  • For follow:

    Must be 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"]}
  • For add_activities:

    Must be 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"}]}

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
Batch Add Activities to Many Feeds 120 1,440 5,760

Performance, Latency & Regions

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

In general, expect your feed to load in about 60ms. The most common cause of slower performance is latency. You can improve this by selecting a data center close 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 intializing 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');
ClientConfiguration streamConfig = new ClientConfiguration().setRegion(StreamRegion.US_EAST);
StreamClient streamClient = new StreamClientImpl(streamConfig, "API_KEY", "API_SECRET");
// 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 & Slow 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).

Our framework/ORM integrations can also handle enrichment for your automatically. Have a look at our enrichment documentation.

Postman and the Stream API

Postman is a popular tool for developing and testing RESTful web-services and APIs.

While we recommend developing for the Stream API via an API Client in your language of choice, it is also often helpful to review and run the low-level HTTP requests that are associated with our APIs. Postman is a great tool for achieving this.

Get started or log in to download the Postman Collection.

Using the Collection

  1. Import the Collection into Postman.
  2. Browse the Collection folders and select a request to send. Review the request 'params' area and 'body' tab for request specific query string parameters or payload data.
  3. Click the 'Send' button to initiate the request and check on the response panel to see the results.

Note: If you're not already familiar with Postman, we recommend the excellent documentation. In particular, the sections on importing Collections offers useful guidance.

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);

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);
// 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
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);
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);
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
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)

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
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.

Collections

Collections enable you to sync information to Stream that is not captured by analytics or feeds. Examples include user profiles and product information. To sync this data, use the following 3 endpoints:

  • Upsert
  • Delete
  • Select

The example below shows how to update or insert a user profile via the upsert command. You can update a max of 1000 items at once. If you need to update more than 1000 items you'll need to make multiple requests.

# Update the information for user with ID 123
client.collections.upsert('user', [{id: '123', name: 'johndoe', favorite_color: 'blue'}])
client.collections.upsert('user', [
    {id: '123', name: 'johndoe', favorite_color: 'blue'}
]).then(
  (resolution) => {
    console.log(resolution);
  ),
  (rejection) => {
    console.log(rejection);
  }
});
# Update the information for user with id 123
client.collections.upsert('user', [{'id': '123', 'username': 'johndoe', 'favorite_color': 'blue'}])
// Update the information for user with id 123
$client->collections()->upsert('user', [
    [
        'id' => '123',
        'username' => 'johndoe',
        'favorite_color' => 'blue',
    ]
]);
// Update the information for user with ID 123
object := stream.CollectionObject{
	ID:   "123",
	Data: map[string]interface{}{
		"name": "johndoe",
		"favorite_color": "blue",
	},
}
err = client.Collections().Upsert("user", object)
if err != nil {
	panic(err)
}
// Update the information for user with ID 123
var obj = new CollectionObject("123");
obj.SetData("name", "johndoe");
obj.SetData("favorite_color", "blue");
await client.Collections.Upsert("user", obj);

Verify Stream is storing the correct data by using the select endpoint. A similar syntax can be used for removing the data.

# Read the data for user 123
objects = client.collections.get('user', ['123'])

# Delete the record for user 123
client.collections.delete('user', ['123'])
// 1. select the record for user with ID 123
objects = client.collections.select('user', ['123']).then(
  (resolution) => {
    resolution["response"]["data"].forEach((obj) => console.log(obj));
    // 2. Delete the record for user with ID 123
    client.collections.delete('user', ['123']);
  },
  (rejection) => {
    console.log(rejection);
  }
# Read the data for user 123
user = client.collections.select('user', [123])
# Delete the record for user 123
response = client.collections.delete('user', [123])
// Read the data for user 123
$user = $client->collections()->select('user', [123]);

// Delete the record for user 123
$response = $client->collections()->delete('user', [123]);
// Read the data for user 123
objects, err := client.Collections().Get("user", "123")
if err != nil {
	panic(err)
}

// Delete the record for user 123
err = client.Collections().Delete("user", "123")
if err != nil {
	panic(err)
}
// Read the data for user 123
var user = await client.Collections.Select("user", "123");

// Delete the record for user 123
await client.Collections.Delete("user", "123");
Note: When you delete an entry from a collection any references will be converted to a missing reference error when reading feeds with enrichment.

Stream on browser and mobile apps

Before we described how to use stream from your backend. But Stream APIs can also be used directly from the browser or from your mobile app. On the frontend Stream currently supports Javascript and has pre built React Native components.

The APIs on the frontend work a bit different than the ones on the backend. The reasons for this are security and ease of use related. On the security front you have to generate a token for each user on the server. This is needed to make sure a user is not allowed to do everything in your apps, for instance editing other users their posts.

Another thing is that it's very easy to use Stream Collections to store data for users and other objects. When doing this, data enrichment happens automatically on the Stream side.

User tokens and user sessions

Even though this part of the docs is about the frontend integration you also need a small backend part to use it. Specifically you need your backend to generate user session tokens. This is token should usually be generated when a user logs in.

With the backend client that you set up at the start of the docs we can create the user session token:

let userId = 'test-user-1';
let userToken = client.createUserSessionToken(userId);
user_id = 'test-user-1'
client.create_user_session_token(user_id)
$user_id = 'test-user-1';
$client->createUserSessionToken($user_id);
userId String = "test-user-1";
streamClient.getUserSessionToken(userId);

With this token we can then then create a user session on the frontend. We will then contact the Stream API with this user session throughout the rest of the docs.

// Install the client using:
// npm install getstream --save
let stream = require('getstream');

// Instantiate a new client (client side)
client = stream.connect('YOUR_API_KEY', null, 'APP_ID');
let userSessionToken = 'USER_SESSION_TOKEN'
let userSession = client.createUserSession(userSessionToken)
Note: You don't have to specify the userId on the client side, because it's part of the token itself.

Adding users

To get the most out of Stream you should add data on the users from your app to Stream. This data will then automatically be added to your feed when reading it later.

Parameters

Name Type Description
data object The data related to the user
// The userSession object has a user element, which contains the current user
let currentUser = userSession.user;
let response = await currentUser.create({
    name: "John Doe", 
    occupation: "Software Engineer",
    gender: 'male'
})

// any request will update the data and the full field in the user instance
currentUser.data == response.data // true
currentUser.full == response // true
 
// You can also only create the user if it does not exist yet
// and otherwise return the current data for the user
// This can be useful to initialize the user data
let response = await currentUser.getOrCreate({name: 'Default Name', occupation: 'Unkown'})
// In this case the user will not have changed will
response.data.name // 'John Doe'
Note: in order to keep the example compact we are using async/await syntax, this is not required.
Note: a user can not add or change data for other users only for itself.

Retrieving users

After adding data for a user you can also retrieve this data easily.

let response = await userSession.user.get()

// To read other users you have to get a new user instance with a specific id 
// and then call the get method on that user instance
let responseForUser123 = await userSession.getUser('123').get()

Deleting users

User data can also be removed.

await userSession.user.delete()
Note: When you delete a user be converted to a missing reference error when enriching.

Updating users

User data can also be updated.

Parameters

Name Type Description
data object The data related to the user
await userSession.user.update({name: "Jane Doe", occupation: "Software Engineer", gender: 'female'})
user.data.name // 'Jane Doe'

Reading feeds with 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 of the backend docs 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. This process of replacing objects to refs and refs to objects is called "Enrichment".

When using Stream on the backend "Enrichment" is relatively simple to implement yourself. First you read the feed from Stream. Then you find all references and retrieve them from your database. You could do the same on the frontend as well, but this would require you to do an extra request to your own backend to return the actual objects based on the reference. However, this is not needed because Stream allows you to do automatic enrichment when reading feeds using the frontend APIs.

Enrichment of users

The most common way of enriching your feeds is by adding user data to it. Here we will show you how to do this, but first let's show how to create feed instances.

// To create a notification feed for the current user you can do
let notifications = userSession.feed('notification')

// The first argument is optional and will default to 'timeline'
let timeline = userSession.feed()

// You can also create a feed object for a feed of  another user
// This can be needed for instance when you looking at their profile
let batman = userSession.feed('user', 'batman')

With these feeds you can then add activities containing user instances.

// We're going to post to the current user's user feed
let userFeed = userSession.feed('user');

// The userSession has a user attribute which is a user instance
let testUser = userSession.user

// We can create the user to add some data to it
await testUser.create({name: 'Test User'})

// You can also get other user objects by their ID (you cannot update their data though)
let amazingUser = userSession.getUser('amazing-user')

// Just like a collection entry instance we can use user instances inside an activity 
// and they will be changed to a reference.
await userFeed.addActivity({
    actor: testUser,
    verb: "loves",
    object: amazingUser,
})

// if we now read the feed, it will contain the activity 
// with the references replaced with the data for the users
let response = await userFeed.get({limit: 10})
Note: Enrichment when reading feeds is only done when reading it using a userSession.

Enrichment of collection entries

Apart from enriching users you can also use Stream Collections to enrich your activities. Stream Collections allows you to store arbitrary unstructured data feeds using a dedicated API endpoint called Collections. You can find more information about Collections here; for now let's just think of it as a K/V store or a MongoDB collection.

// This is the object we want to store in the activity
let cheeseBurgerData = {
    name:        'Cheese Burger',
    ingredients: ['cheese', 'burger', 'bread', 'lettuce', 'tomato']
}

// We can add an object to Stream collections using .add(id, {obj}) 
// (you can specify null/undefined for the id to let stream generate one)
let cheeseBurgerResponse = await userSession.collection('food').add('123', cheeseBurgerData)
// Then you can convert that response to a collection entry instance
let cheeseBurger = userSession.collectionEntryFromResponse(cheesBurgerResponse)

// When you add the collection entry instance it will automatically be converted to a reference
await userFeed.addActivity({actor: userSession.user, verb: 'grill', object: cheeseBurger})

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

// Now that we have a collection entry we can use it in multiple activities
await userFeed.addActivity({actor: userSession.user, verb: 'eat', object: cheeseBurger})

// And then we can update the collection entry to effectively change all activities at once
await cheeseBurger.update({
    name:        'Amazing Cheese Burger',
    ingredients: ['cheese', 'burger', 'bread', 'lettuce', 'tomato']
})

Creating references in the backend

One thing that is quite common when you use Stream both on the backend and frontend is that you want to be able to create users and collection entries on the backend. And also, you want to be able to create references to them on the backend.

// To create a collection entry you can use the upsert api
await client.collections.upsert('food', [
    {id: 'cheese-burger', name: 'Cheese Burger'}
])

// To create a user simply add data to the user collection
await client.collections.upsert('user', [
    {id: '123', name: 'John Doe', favorite_color: 'blue'}
])

// Then you can create references to them
let cheesBurgerRef = client.collections.createReference('food', 'cheese-burger')
let johnDoeRef = client.collections.createUserReference('123')

// And then you can add activities that contain these references
client.feed("user", "john").addActivity({
    actor: johnDoeRef,
    verb: 'eat',
    object: cheeseBurgerRef,
})
Note: More information on using the backend collections API can be found here.

Adding collection entries

Collections on the frontend work a bit different than collections on the backend. The main difference is that you have a collection instance that you add entries to.

Parameters

Name Type Description Optional
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
await userSession.collection("food").add('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 an ID for you
// hotdog will now an ID generated by Stream
await userSession.collection("food").add(null, {name: "Hotdog", rating: "3 stars"})

Retrieving collection entries

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

Parameters

Name Type Description
entry_id string The id of the entry
let response = await userSession.collection("food").get('cheese-burger')

Deleting collection entries

An entry can also be removed from a collection

Name Type Description
entry_id string The id of the entry
let response = await userSession.collection("food").delete('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

User data can also be updated.

Parameters

Name Type Description
entry_id string The id of the entry
data object The data related to the user
await userSession.collection("food").update('cheese-burger', {name: "Cheese Burger", rating: "1 star"})

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 when reading feeds within a user session (see section below for more information and examples).

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
userSession.reactions.add("like", activityId);

// adds a comment reaction to the activity with id activityId
userSession.reactions.add("comment", activityId, {"data": {"text": "awesome post!"}});

Here's a complete example:

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

// then let's add a like reaction to that activity
await userSession.reaction.add("like", activity.id);
Note in order to keep the example compact we are using async/await syntax, this is not required.

Delete Reactions

Reactions can be removed by their ID

userSession.delete(reactionId);

Update 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
target_feeds string
userSession.reactions.update(reactionId, {data: {"text": "love it!"}});

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
userSession.reactions.add("comment", activityId, {
    "data": {"text": "@thierry great post!"},
    "targetFeeds": ["notification:thierry"],
});

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
withRecentReactions boolean include the 10 most recent reactions to activities
withOwnReactions boolean include current user's reaction to activities
withReactionCounts boolean include the total count of reaction (by kind) to activities
// read user's timeline and include most recent reactions to all activities and their total count
userSession.feed('timeline').get({
    withRecentReactions: true,
    withReactionCounts: true,
});

// read user's timeline and include most recent reactions to all activities and her own reactions
userSession.feed('timeline').get({
    withOwnReactions: true,
    withReactionCounts: true,
});

Files and Images

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

// user sessions are commonly used client-side (eg. web / react native)
// the userToken variable is created server-side (often as part of part of the sign-up / sign-in flow)
// var userToken = client.createUserSessionToken(userId);

userSession = client.createUserSession(userId, userToken);
In order to upload files and images client-side you first need to create a user session; a user session exposes the same functionality as the regular API client (it can be used to read feeds, add activities, ...).
At this moment user sessions are only supported by the Javascript client.

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)
userSession.images.upload(input.files[0]);

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

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

// uploading a file from React Native (fileURI is the URI of the file)
userSession.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
userSession.images.delete(imageURL);

// deleting a file using the url returned by the APIs
userSession.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
userSession.images.process(imageURL, {h:50, w:50, resize:'crop'});

// create a 50x50 thumbnail using clipping (keeps aspect ratio)
userSession.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.

// user sessions are commonly used client-side (eg. web / react native)
// the userToken variable is created server-side (often as part of part of the sign-up / sign-in flow)
// var userToken = client.createUserSessionToken(userId);
userSession = client.createUserSession(userId, userToken);

userSession.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.

To listen to feed changes using websockets you need a feed token. The example below shows you how to get a read only token.
# read + write token for feed user:1
token = client.feed('user', '1').token
# Generating tokens for client side usage
readonly_token = client.feed('user', '1').readonly_token
// read + write token for feed user:1
var token = client.feed('user', '1').token;
// creates a readonly token for feed user:1
var readonlyToken = client.feed('user', '1').getReadOnlyToken();
# read + write token for feed user:1
token = user_feed_1.token
# readonly token
readonly_token = user_feed_1.get_readonly_token()
// read + write token for feed user:1
$token = $userFeed1->getToken();

// creates a read-only token for feed user:1
$readonlyToken = $userFeed1->getReadonlyToken();
// read and write token for feed user:1
String token = userFeed1.getToken();

// readonly token
String readOnlyToken = userFeed1.getReadOnlyToken();
// read + write token for feed user:1
token := userFeed.RealtimeToken(false)
// readonly token
readonlyToken := userFeed.RealtimeToken(true)

As a next step you want to use this token in your Javascript code and listen to feed changes:

<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.