Introduction

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, Meteor 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 Microsoft .NET:

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
{
    "require": {
        "get-stream/stream": "$VERSION"
    }
}
// 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 github.com/GetStream/stream-go

// import it in your code
import "github.com/GetStream/stream-go"
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 (
	"github.com/GetStream/stream-go"
)

client, err := getstream.New(&getstream.Config{
	APIKey:      "YOUR_API_KEY",
	APISecret:   "API_KEY_SECRET",
	AppID:       "APP_ID",
	Location:    "us-east",
})
if err != nil {
	return err
}
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: add as many custom fields as you'd like!
activity_data = { :actor => 'chris', :verb => 'add', :object => 'picture:10', :foreign_id => 'picture:10', :message => 'Cool bird.' }
chris.add_activity(activity_data);

# jack's 'timeline' feed follows chris' 'user' feed.
jack = client.feed('timeline', 'jack')
jack.follow('user', 'chris')

# Read the 'timeline' for jack, chris' post will now show up here:
activities = jack.get(:limit => 10)

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


# Remove the activity by referencing the foreign_id you provided
chris.remove_activity('picture:10', foreign_id=true)
var stream = require('getstream');
var client = stream.connect('KEY', 'SECRET');

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

// Add 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. Absolutely beautiful. Phenomenal bird.'
});

// jack's 'timeline' feed follows chris' 'user' feed:
var jack = client.feed('timeline', 'jack');
jack.follow('user', 'chris');

// Read 'timeline' for jack - the post by chris will show up:
jack.get({ limit: 10 }).then(function(results) {
  var activityData = results;

  // Read the next page, using id filtering for optimal performance:
  jack.get({ limit: 10, id_lte: activityData[activityData.length-1].id }).then(function(results) {
    var nextActivityData = results;
  });
});

// Remove activity by referencing foreign_id:
chris.removeActivity({ foreign_id: 'picture:10' });
chris = client.feed('user', 'chris')
# add an activity, message is a custom field. add as many custom fields as you'd like
chris.add_activity({
  'actor': 'chris',
  'verb': 'add',
  'object': 'picture:10',
  'foreign_id': 'picture:10',
  'message': 'This bird is absolutely beautiful. Glad it\'s recovering from a damaged wing.'
});
# jack's timeline feed follows chris' user feed.
jack = client.feed('timeline', 'jack')
jack.follow('user', 'chris')
# read the timeline for jack, chris post will show up here
activities = jack.get(limit=10)['results']
# read the next page, use id filtering for optimal performance
next_activities = jack.get(limit=10, id_lte=activities[-1]['id'])['results']
# remove the activity by referencing the foreign_id you provided
chris.remove_activity(foreign_id='picture:10')
$chris = $client->feed('user', 'chris');

// Add an activity; message is a custom field - tip: add unlimited custom fields!
$data = array(
  "actor" => "chris",
  "verb" => "add",
  "object" => "picture:10",
  "foreign_id" => "picture:10",
  "message" => "Beautiful bird. Absolutely beautiful. Phenomenal bird."
);

$chris->addActivity($data);


// jack's 'timeline' feed follows chris' 'user' feed:
$jack = $client->feed('timeline', 'jack');
$jack->followFeed('user', 'chris');


// Read the 'timeline' feed for jack, chris' post will now show up:
$activities = $jack->getActivities(10);

// Read the next page, use id filtering for optimal performance:
$last_activity = end($activities);
$options = array("id_lte" => $last_activity['id']);
$next_activities = jack->getActivities(10, $options);

// Remove the activity by referencing the foreign_id you provided:
$chris->removeActivity("picture:10", true);
import (
	"github.com/pborman/uuid"
	"github.com/GetStream/stream-go"
)

ianFeed, err := client.FlatFeed("flat", "ian")
if err != nil {
	fmt.Println(err)
	return
}

for i := 0; i < 15; i++ {
	_, err := ianFeed.AddActivity(&getstream.Activity{
		Verb:      "post",
		ForeignID: uuid.New(),
		Object:    getstream.FeedID("flat:ian"),
		Actor:     getstream.FeedID("flat:jack"),
		MetaData:  map[string]string{
			// add as many custom keys/values here as you like
			"message": fmt.Sprintf("message %d", i),
		},
	})
	if err != nil {
		fmt.Println(err)
	}
}

jackFeed, err := client.FlatFeed("flat", "jack")
if err != nil {
	fmt.Println(err)
}

// jack's timeline feed follows ian's feed.
jackFeed.FollowFeedWithCopyLimit(ianFeed, 100)

// read the timeline for jack, ian's posts will show up here
activities, err := jackFeed.Activities(&getstream.GetFlatFeedInput{Limit: 10, Offset: 0})
if err != nil {
	fmt.Println(err)
}
for _, activity := range activities.Activities {
	fmt.Println(activity)
}

// read the next page, use id filtering for optimal performance
activities, err := jackFeed.Activities(&getstream.GetFlatFeedInput{IDLT: activities.Activities[len(sl)-1]}, Limit: 10)
if err != nil {
	fmt.Println(err)
}
for _, activity := range activities.Activities {
	fmt.Println(activity)
	// remove any activities that we read this time
	jackFeed.RemoveActivityByForeignID(activity)
}

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 four basic properties:

  • Actor
  • Verb
  • Object
  • Target (Optional)

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)

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)
// Instantiate a feed using feed class 'user' and user id '1'
var user1 = client.feed('user', '1');
// Instantiate a feed for feed group 'user', user id '1' and a security token generated server side
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
user2.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
$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`)
$data = [
    "actor"=>"User:1",
    "verb"=>"pin",
    "object"=>"Place:42",
    "target"=>"Board:1"
];
$user_feed_1->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
userFeed1, err := client.FlatFeed("User", "bobby")
if err != nil {
	return err
}
// Add an activity to the feed, where actor, object and target are references to objects (`Eric`, `Hawaii`, `Places to Visit`)
// this code assumes you have set up other feeds for Place and Board
activity, err := ianFeed.AddActivity(&getstream.Activity{
	Verb:    "post",
	Object:  getstream.FeedID("Place:42"),
	Actor:   getstream.FeedID("User:eric"),
	Target:  getstream.FeedID("Board:1"),
})
if err != nil {
	fmt.Println(err)
}

Listed below are all the parameters for adding activities.

Parameters

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 optional time of the activity, isoformat (UTC localtime) 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
* string/list/object/point Add as many custom fields as needed

Adding Activities: Custom fields & Geolocations

Using the above fields, you can express any activity. Stream also allows you to add custom fields to the activity.

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.

Let's have a 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.now(),
    '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", new \DateTimeZone('Pacific/Nauru'));
$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] ]
];
$user_feed_1->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
// our current Go client allows for a map[string]string data type 
// within the MetaData field, so any advanced data types you wish
// to use should be serialized/unserialized in whichever way is
// most appropriate for your application

course, _ := json.Marshal(map[string]interface{}{"name": "Golden Gate park", "distance": 10})
participants, _ := json.Marshal([]string{"Thierry", "Tommaso"})
started_at := time.Now().Format("2006-01-02T15:04:05.999999")
location, _ := json.Marshal(map[string]interface{}{"type": "point", "coordinates": []float64{37.769722, -122.476944}})

activity := &getstream.Activity{
	Verb:      "run",
	Object:    getstream.FeedID("Exercise:42"),
	Actor:     getstream.FeedID("User:1"),
	ForeignID: uuid.New(),
	MetaData:  map[string]string{
		"participants": string(participants),
		"course":       string(course),
		"started_at":   string(started_at),
		"location":     string(location),
	},
}
userFeed1.AddActivity(activityData)

Note how the custom fields course, location, participants and started_at just worked. Also have a look at how the location field uses a point as a value. (Read more about Stream's Geolocation support)


Using Foreign IDs

The example above also specified a foreign_id.

The foreign id allows you to send Stream a unique id representing this activity within your app. We highly recommend sending it as it helps us enforce uniqueness and makes it easier to delete activities.

The server also returns an 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
user_feed_1.remove_activity("e561de8f-00f1-11e4-b400-0cc47a024be0")
# Remove activities with foreign_id 'run:1'
user_feed_1.remove_activity(foreign_id='run:1')
// Remove an activity by its id
$user_feed_1->removeActivity("e561de8f-00f1-11e4-b400-0cc47a024be0");

// Remove activities with foreign_id 'run:1'
$user_feed_1->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");
activity = &getstream.Activity{ ... }
userFeed1.AddActivity(activity)

// Remove an activity by its id
userFeed1.RemoveActivity(activity)

// Remove activity with ForeignID 'run:1'
userFeed1.RemoveActivityByForeignID(&getstream.Activity{ForeignID: "run:1"})
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.add_activity(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": 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])
$now = new DateTime('now');
$activities = array();

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

// add the activity to a feed
$user_feed_1->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
// first time the activity is added
activity, _ = userFeed1.AddActivity(&getstream.Activity{
	Actor: "1",
	Verb: "like",
	Object: "3",
	ForeignID: uuid.New(),
	MetaData: map[string]string{"popularity": "100"},
})

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

// send the update to the APIs
userFeed1.UpdateActivity(activity)
Note: It is not possible to update more than 100 activities per request with this method. When you update an activity, you must send both foreign_id and time fields.
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.

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:


// If both time and foreign_id fields are provided: Uniqueness will be 
// enforced on the combination of time & foreign_id.

// Adding foreign_id - Part 1/2 of adding uniqueness:
var activity = {'actor': 1, 'verb': 'add', 'object': 1, 'foreign_id': 'add:1'};


// Add property for time to activity object - Part 2/2 of adding uniqueness:
var now = new Date();
activity.time = now.toISOString();

// Set up feed
var user1 = client.feed('user', '1', token);

// Add activity to activity feed:
user1.addActivity(activity)
    .then(onSuccess)
    .catch(onError);


// Add property for time to activity object - Part 2/2 of adding uniqueness:
var now = new Date();
activity.time = now.toISOString();

// Time is unique:
user1.addActivity(activity)
  .then(onSuccess)
  .catch(onError);


function onSuccess(data) {
  // Unique Id will be returned:
  var activityId = data; 
}

// Result: Activity Ids are unique!
from datetime import datetime
now = datetime.utcnow()

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

second_activity = {
    "actor": "1",
    "verb":"like",
    "object": "3",
    "time": now,
    "extra": "extra_value",
    "foreign_id": "like:3"
}
second_activity_id = user_feed_1.add_activity(activity)['id']
# foreign_id and time are the same for both activities
# hence only one activity is created and first and second id are equal
identicalForeignID := uuid.New()
identicalTimestamp := time.Now()

activity, _ := ianFeed.AddActivity(&getstream.Activity{
	Actor: "1",
	Verb: "like",
	Object: "3",
	TimeStamp: &identicalTimestamp,
	ForeignID: identicalForeignID,
	MetaData: map[string]string{"popularity": "100"},
})
fmt.Println(activity.ID)

activity, _ = ianFeed.AddActivity(&getstream.Activity{
	Actor: "1",
	Verb: "like",
	Object: "3",
	TimeStamp: &identicalTimestamp,
	ForeignID: identicalForeignID,
	MetaData: map[string]string{"popularity": "100"},
})
// the activity record that gets sent back has the same activity.ID value as our first
// activity since the timestamp AND the foreignID are exactly the same
fmt.Println(activity.ID)

Retrieving Activities

The example below explains how to retrieve the activities in the 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 - using limit and offset
$results = $user_feed_1->getActivities(5, 10);

# Get 5 activities with id less than the given UUID (Faster - Recommended!)
$options = array("id_lte" => $last_id);
$results = $user_feed_1->getActivities(0, 5, $options);

# Get 5 activities - using limit, offset - and sorted by rank (Ranked Feeds Enabled):
$options = array("ranking" => "popularity");
$results = $user_feed_1->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!)
activities, err = userFeed1.Activities(&getstream.GetFlatFeedInput{
	Limit: 5,
	IDLT: "e561de8f-00f1-11e4-b400-0cc47a024be0",
})

// Get activities from 5 to 10 (Pagination-based - Slower)
activities, err = userFeed1.Activities(&getstream.GetFlatFeedInput{
	Limit: 5,
	Offset: 5,
})

// Get activities sorted by rank (requires Ranked Feeds to be enabled):
activities, err = userFeed1.Activities(&getstream.GetFlatFeedInput{
	Limit: 5,
	Ranking: "popularity",
})

The activities in the feed are sorted on a UUID based on time. We recommend you paginate using id_lt. This performs far better than traditional offset based pagination. The full list of supported parameters is included below.

Note: API reads returns at most 100 activities, requests with limits higher than 100 will be capped automatically to return 100 activities.

Parameters

Name Type Description Default Optional
limit int The amount of activities requested from the APIs 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 Filter the feed by custom ranking formula, defined in the dashboard

Go Naming practices

The flags described above may be named differently to conform to Go standards. Be sure to check our Go client repository (link) for our naming practices.

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

Following Feeds

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

# timeline:1 follows user:42
timeline_1 = client.feed('timeline', '1')
timeline_1.follow('user', '42')

# follow feed without copying the activities:
timeline_1.follow('user', '42', :activity_copy_limit=>0)

# you're not restricted to following users, follow any feed
timeline_1.follow('playlist', '10')

# follow feed without copying the activities
timeline_1.follow('playlist', '10', :activity_copy_limit=>0)
// timeline:1 follows user:42:

var timeline_1 = client.feed('timeline', '1');
timeline_1.follow('user', '42');

// follow feed without copying the activities:
timeline_1.follow('user', '42', { limit: 0 });

// Note: you are not restricted to following users - follow any feed (such as a playlist):
timeline_1.follow('playlist', '10')
# timeline:1 follows user:42
timeline_1 = client.feed('timeline', '1')
timeline_1.follow('user', '42')
# you're not restricted to following users, follow any feed
timeline_1.follow('playlist', '10', activity_copy_limit=20)
# timeline:1 follows user:42
$timeline_1 = $client->feed('timeline', '1');
$timeline_1->followFeed('user', '42');
# you're not restricted to following users, follow any feed
$timeline_1->followFeed('playlist', '10')

# follow feed without copying the activities:
$timeline_1->followFeed('playlist', '10', 0)
// timeline:1 follows user:42
Feed timeline_1 = streamClient.newFeed("timeline", "1");
timeline_1.follow("user", "42");
// you're not restricted to following users, follow any feed
timeline_1.follow('playlist', '10')
timeline1 = client.FlatFeed("timeline", "1")
userFeed42 = client.FlatFeed("flat", "42")
aggFeed73 = client.AggregatedFeed("playlist", "10")

// timeline:1 follows user:42, but don't copy previous posts
timeline1.FollowFeedWithCopyLimit(userFeed42, 0)

// you're not restricted to following users, follow any feed
// this example copies 20 of the most recent posts
timeline1.FollowFeedWithCopyLimit(aggFeed73, 20)

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

Allows a feed to stop following the feed specified in the target parameter. An example below:

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

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

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

# Stop following feed 42 but keep history of activities
timeline_feed_1.unfollow('user', '42', keep_history=True)
# Stop following feed 42
timeline_feed_1->unfollow('user', '42');

# Stop following feed 42 but keep history of activities:
timeline_feed_1->unfollow('user', '42', true);
// Stop following feed 42
timeline_feed_1.unfollow("user", "42");

// Stop following feed 42 keeping the history
timeline_feed_1.unfollow("user", "42", true);
// Stop following user feed 42
timelineFeed1.Unfollow(userFeed42)

// Stop following user feed 42 but keep history of activities
timelineFeed1.UnfollowKeepingHistory(userFeed42, keep_history=True)

// Stop following an aggregated feed:
timelineFeed1.UnfollowAggregated(aggFeed123)

// Stop following an aggregated feed:
timelineFeed1.UnfollowNotification(notFeed972)

// we do not yet support unfollowing an aggregated feed or notification feed and keeping history

Existing activities in the feed coming from the target 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. Our Go client does not support unfollowing aggregated feeds and notification feeds and keeping those histories.

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

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
$user_feed_1->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 of this feed
limit := 10
offset := 0
userFeed1.FollowersWithLimitAndSkip(offset, limit)

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 400 0

Reading Followed Feeds

Returns a paginated list of the feeds which are following 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, filter=['user:42', 'user:43'])
// Retrieve 10 feeds followed by $user_feed_1
$user_feed_1->following(0, 10);

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

// Check if $user_feed_1 follows specific feeds
$user_feed_1->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 userFeed1
offset := 0
limit := 10
userFeed1.FollowingWithLimitAndSkip(offset, limit)

// Retrieve 10 feeds followed by userFeed1 starting from the 11th
userFeed1.FollowingWithLimitAndSkip(offset+10, limit)

// our Go client does not support filters passed to FollowingWithLimitAndSkip()

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 400 0
filter string The comma separated list of feeds to filter results on

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

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.id }}_{{ 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.id the unique internal id (generated by GetStream) to the activity verb (e.g. 42)
  • verb.infinitive the 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 (e.g. timeline)
  • feed_id the feed id (e.g. 123)

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.id }}_{{ time.strftime("%Y-%m-%d") }}
  • Per target, verb id and day:
    {{ target }}_{{ verb.id }}_{{ time.strftime("%Y-%m-%d") }}
  • Follows by actor, likes by target
    {% if verb.infinitive == 'follow' %}{{ actor }}{% else %}{{ target }}{% endif %}_{{ verb.id }}_{{ time.strftime("%Y-%m-%d") }}

The aggregation rule uses the Jinja2 Template Syntax. The options for strftime are documented here.

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. The actor and object count do increase past 15.

Notification Feeds

Notification feeds are similar to aggregated feeds. There are two key differences which make them more suited to building notification systems:

  • Notifications can be marked as seen or read.
  • You get a real-time count of the number of unseen and unread notifications.

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 {{ verb.id }}_{{ time.strftime("%Y-%m-%d") }}

The seen and read fields allow you to track different types of user interaction. 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 set the mark read and seen when reading the feed:

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

Here's the example using the various clients:

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

# Mark some activities as read
result = notification_feed.get(:limit=>5, :mark_read=>['activity_id_1', 'activity_id_2'])
// Mark all activities as seen
notificationFeed.get({limit:5, mark_seen:true})
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });
// Mark some activities as read
notificationFeed.get({limit:5, mark_read: ['activityIdOne', 'activityIdTwo']})
    .then(function(data) { /* on success */ })
    .catch(function(reason) { /* on failure */ });
# Mark all activities as seen
result = notification_feed.get(limit=5, mark_seen=True)
# Mark some activities as read
result = notification_feed.get(limit=5, mark_read=[activity_id, activity_id_2])
# Mark all activities as seen
$options = array('mark_seen' => true);
$results = $notification_feed->getActivities(0, 10, $options);

# Mark some activities as read
$options = array('mark_read' => array('activity_id_1', 'activity_id_2'));
$results = $notification_feed->getActivities(0, 10, $options);
// Mark all activities 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());
notificationFeed, err := getstream.NotificationFeed("notifications", "userID")

// Mark 5 activities as seen
limit := 5
err = notificationFeed.MarkActivitiesAsSeenWithLimit(limit)

// Mark some activities as read
activities := []*getstream.Activity{...}
err = notificationFeed.MarkActivitiesAsRead(activities)

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.

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 = $user_feed_1-> getToken();
// creates a readonly token for feed user:1
$readonlyToken = $user_feed_1->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 = userFeed1.Token()

// readonly token is not yet supported in the Go client library

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="/path/to/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 Log Function natural base
log Log Function base 10
decay_linear Linear Decay
decay_exp Exponential Decay
decay_gauss Gauss Decay

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
$user_feed_1 = $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
];


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

// Add an activity to the feed, where actor, object and target are references to objects,
// also adding your ranking method as a parameter (in this case, "popularity"):
activity := &getstream.Activity(
	Actor: "User,:2"
	Verb: "pin",
	Object: "Place:42",
	Target: "Board:1",
	MetaData: map[string]string{"popularity": "5"},
}

activityResponse = userFeed1.AddActivity(activity)

Retrieving Activities

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

# Get activities sorted by 'popularity' rank (Ranked Feeds Enabled):
result = user_feed_1.get(:limit=>5, :ranking=>'popularity')
/*
 * Get activities sorted by 'popularity' rank (Ranked Feeds Enabled)
 */
user1.get({ limit:20, ranking:'popularity'})
  .then(activitiesSuccess)
  .catch(activitiesError);
# Get activities sorted by 'popularity' rank (Ranked Feeds Enabled):
result = user_feed_1.get(limit=5, ranking="popularity")
# Get 5 activities - using limit, offset - and sorted by 'popularity' rank (Ranked Feeds Enabled):
$options = array("ranking" => "popularity");
$results = $user_feed_1->getActivities(0, 5, $options);
// get activities sorted by 'popularity' rank (requires Ranked Feeds to be enabled)

activities = userFeed1.Activities(&getstream.GetFlatFeedInput{
	Limit: 5,
	Ranking: "popularity",
})
Please note: offset and id_lt cannot be used to read ranked feeds. Use score_lt for pagination instead.

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 or 0
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 or 5
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.

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.

# Instantiate a new client
import stream
client = stream.connect('YOUR_API_KEY', 'API_KEY_SECRET')
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')
// make a feed for Chris
timelineChris, err := client.FlatFeed("timeline", "chris")

// Chris follows a friend, but don't copy any items yet
timelineJack, err := client.FlatFeed("timeline", "jack")
timelineChris.FollowFeedWithCopyLimit(timelineJack, 0)

// follow a playlist, copy last 10 items
playlist90s, err := client.FlatFeed("playlist", "90s")
timelineChris.FollowFeedWithCopyLimit(playlist90s, 10)

// follow an artist
artistColdplay, err := client.FlatFeed("artist", "Coldplay")
timelineChris.FollowFeedWithCopyLimit(artistColdplay, 10)

Step 3: Posting an update

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

# Jack posts an update with a hashtag and @mention
jack = client.feed('user', 'jack')
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'
});
chrisFeed := client.FlatFeed("user", "chris")
tagFeed := client.FlatFeed("tag", "amsterdam")

// Jack posts an update with a hashtag and @mention
jackFeed.AddActivity(&getstream.Activity{
	Actor: 'jack',
	Verb: 'post',
	Object: 'post:10',
	ForeignID: 'post:10',
	To: []getstream.Feed{chrisFeed, tagFeed},
	MetaData: map[string]string{
		"message": "Hi @chris, the coldplay concert was totally amazing #amsterdam"
	},
})

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
chrisFeed, err := client.FlatFeed("user", "chris")
jackNotificationFeed := client.NotificationFeed("notification", "jack")
activity, err := chrisFeed.AddActivity(&getstream.Activity{
	Actor: "chris",
	Verb: "like",
	Object: "post:10",
	ForeignID: "like:25",
	To: []getstream.Feed{jackNotificationFeed},
});
// this will 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.

# read the timeline for jack
jack = client.feed('timeline', '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
jackFeed, err := client.FlatFeed("user", "jack")
activities, err := jackFeed.Activities(&getstream.GetFlatFeedInput{Limit: 10})

// read the next page, use id filtering for optimal performance
activities, err = jackFeed.Activities(&getstream.GetFlatFeedInput{
	Limit: 10,
	IDLT: activities[len(activities)-1].ID,
})

// reading a notification feed is very similar
chrisNotificationFeed, err := client.NotificationFeed("user", "chris")
activities, err := chrisNotificationFeed.Activities(&getstream.GetFlatFeedInput{Limit: 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 = $user_feed_1-> getToken();
// creates a readonly token for feed user:1
$readonlyToken = $user_feed_1->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 = userFeed1.Token()

// readonly token is not yet supported in the Go client library

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>

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"]
];
$user_feed_1->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
feed1, err := client.FlatFeed("user", "eric")
feed2, err := client.NotificationFeed("notification", "jessica")

activity := getstream.Activity{
	Actor: ericFeed.FeedID(),
	MetaData: map[string]string{
		"message": "@Jessica check out getstream.io it's so dang awesome!",
	},
	Verb: "tweet",
	Object: "tweet:id",
        To: []getstream.Feed{feed2},
};
activity, err = feed1.AddActivity(activity);

// tip: 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']
];
$player_feed_1->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);
feed1, err := client.FlatFeed("team", "Netherlands")
feed2, err := client.NotificationFeed("match", "NEDvsCAN")
feed3, err := client.FlatFeed("Player", "Sneijder")
feed4, err := client.FlatFeed("Player", "Thomas")

// The TO field ensures the activity is sent to the player, match and team feed
activity = &getstream.Activity{
	Actor: feed3.FeedID(),
	Verb: "foul",
	Object: feed4.FeedID(),
        To: []getstream.Feed{feed1, feed2},
	MetaData: map[string]string{
		"match": "{\"name": \"El Clasico\", \"id\": 10}",
	},
}
activity, err = feed4.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 a copy of the activity stored in your own database. It makes it easier to sync updates and it allows you run custom reporting and queries on your end.
  • Store a user id and not the full user object. Otherwise if someone changes their user image or username you'll have to update tons of activities.
  • Attach metadata like tags and categories (if applicable) to your activities. This will help our data-science team optimize your feed personalization algorithms. (available on enterprise plans).
  • If you're using ranked feeds we recommend storing counts of the likes and comments. That allows you to take those factors into account when sorting feeds.
  • Activities have a max size. Storing a blob of text is ok, storing images directly in the activity definitely won't work though.

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

You'll often store references in your activities. When displaying the feed you'll need to lookup a reference like "user:123" and translate that into the corresponding userObject. Querying all of those references in as few queries as possible can be pretty tricky. That's why we've automated it for you using some of the most popular ORMs.

To clarify, we want to translate this:

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

Into this:

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

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.now(),
    '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", new \DateTimeZone('Pacific/Nauru'));
$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] ]
];
$user_feed_1->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
// our current Go client allows for a map[string]string data type 
// within the MetaData field, so any advanced data types you wish
// to use should be serialized/unserialized in whichever way is
// most appropriate for your application

course, _ := json.Marshal(map[string]interface{}{"name": "Golden Gate park", "distance": 10})
participants, _ := json.Marshal([]string{"Thierry", "Tommaso"})
started_at := time.Now().Format("2006-01-02T15:04:05.999999")
location, _ := json.Marshal(map[string]interface{}{"type": "point", "coordinates": []float64{37.769722, -122.476944}})

activity := &getstream.Activity{
	Verb:      "run",
	Object:    getstream.FeedID("Exercise:42"),
	Actor:     getstream.FeedID("User:1"),
	ForeignID: uuid.New(),
	MetaData:  map[string]string{
		"participants": string(participants),
		"course":       string(course),
		"started_at":   string(started_at),
		"location":     string(location),
	},
}
userFeed1.AddActivity(activityData)

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.

Batch Follow

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.

# 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
// timeline:1 will follow user:1, user:2 and user:3

timeline1, err := client.FlatFeed("timeline", "1")
user1, err := client.FlatFeed("user", "1")
user2, err := client.FlatFeed("user", "2")
user3, err := client.FlatFeed("user", "3")

var follows []getstream.PostFlatFeedFollowingManyInput
follows = append(follows, *client.PrepFollowFlatFeed(timeline1, user1))
follows = append(follows, *client.PrepFollowFlatFeed(timeline1, user2))
follows = append(follows, *client.PrepFollowFlatFeed(timeline1, user3))

// follow the feeds and copy zero items from each
// change the 0 to another positive integer to copy the last 'n' items
timeline1.FollowManyFeeds(follows, 0)

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 = array(
    array('actor' => 'User:1', 'verb' => 'tweet', 'object' => 'Tweet:1'),
    array('actor' => 'User:2', 'verb' => 'watch', 'object' => 'Movie:1')
);
$user_feed_1->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);
// create several activities
activities := []*getstream.Activity{
	&getstream.Activity{
		Verb:      "post",
		ForeignID: uuid.New(),
		Object:    getstream.FeedID("flat:eric"),
		Actor:     getstream.FeedID("flat:john"),
	}, &getstream.Activity{
		Verb:      "walk",
		ForeignID: uuid.New(),
		Object:    getstream.FeedID("flat:john"),
		Actor:     getstream.FeedID("flat:eric"),
	},
})

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: 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 just 1 API request.

Note: The batch method doesn't trigger a fanout - therefore the followers of these feeds won't receive an update.

# 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 = array('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);
// create several activities
activities := []*getstream.Activity{
	&getstream.Activity{
		Verb:      "post",
		ForeignID: uuid.New(),
		Object:    getstream.FeedID("flat:eric"),
		Actor:     getstream.FeedID("flat:john"),
	}, &getstream.Activity{
		Verb:      "walk",
		ForeignID: uuid.New(),
		Object:    getstream.FeedID("flat:john"),
		Actor:     getstream.FeedID("flat:eric"),
	},
})

// the Go client allows you to add multiple activities to a single feed at a time
// it does not support adding multiple activities to multiple feeds in a single method call

feeds = []*FlatFeed
feed, err := client.FlatFeed("flat", "bob")
feeds = append(feeds, feed)
feed, err := client.FlatFeed("flat", "jane")
feeds = append(feeds, feed)

for _, feed := range feeds {
	feed.AddActivities(activities)
}

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)

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

Importing data? We are here to help! Feel free to contact us.

Common Import Scenarios

  1. You want to import only follow relationships. Your feed will show new activities only.
  2. You want to import both follows and activities. Your feed will also show older data.

Best Practices

  1. When importing both activities and follows, start with the activities. Your import will run substantially faster.
  2. Use batch operations where possible: add_activities is a lot quicker than repeated calls via add_activity.
  3. For large imports, always contact support.

Data Dumps

For large imports the best approach is to send us a dump of your data. Our preferred format is JSON, an example of a dump is shown below:

// Start by adding multiple activities - grouped by feedId:
{  
   "instruction":"add_activities",
   "feedId":"user:1",
   "data":[  
      {  
         "actor":1,
         "verb":"tweet",
         "object":1,
         "foreign_id":"tweet:1",
         "time":"2016-04-20T17:47:40.529165"
      },
      {  
         "actor":2,
         "verb":"tweet",
         "object":2,
         "foreign_id":"tweet:2",
         "time":"2016-04-20T17:47:40.529165"
      }
   ]
}

// Follow instructions, grouped by feedId - timeline:1 follows user:2 and user:3:
{"instruction": "follow", "feedId": "timeline:1", "data": ["user:2", "user:3"]}

// Instructions are separated by newlines - timeline:2 follows user:3 and user:4:
{"instruction": "follow", "feedId": "timeline:2", "data": ["user:3", "user:4"]}

Be sure to include the foreign_id and time fields in your activities. These fields are required for running an import Next, gzip the dump and send it to support. The dump allows us to skip the HTTP layer - and will often be 30 times faster than the equivalent imports via the APIs.

Note: Running an import is only available on paid plans.

Performance, Latency & Regions

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

In general, expect your feed to load in about 60ms. Te most common cause of slower performance is latency. You can improve this by selecting a data center close to you.

We currently support these three locations:

  • US East - Virginia: us-east
  • US West - Northern California: us-west
  • Europe West - Ireland: eu-west

To change your location, first get in touch with support. The second step is to update your connection settings 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");
client, err := getstream.New(&getstream.Config{
	APIKey:    "your-api-key",
	APISecret: "your-api-secret",
	AppID:     "your-app-id",
	Location:  "us-east"})
if err != nil { ... }

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.