New product announcement 🎉 Satori - LiveOps to know your players, deliver features, run experiments and schedule events

Leaderboards #

Leaderboards are a great way to add a social and competitive element to any game. They’re a fun way to drive competition among your players. The server supports an unlimited number of individual leaderboards with each one as a scoreboard which tracks separate records.

The server has no special requirement on what the score value should represent from your game. A leaderboard is created with a sort order on values. If you’re using lap time or currency in records you’ll want to order the results in ASC or DESC mode, as preferred. At creation time you must also specify the operator which controls how scores are submitted to the leaderboard: “best”, “set”, “incr”, or “decr”.

You can use a leaderboard to track any score you like. Some good examples are: highest points, longest survival time, fastest lap time, quickest level completion, and anything else which can be competed over!

Leaderboards are dynamic in the server because they don’t need to be preconfigured like would be needed if you’ve used Google Play Games or Apple Game Center in the past. A leaderboard can be created via server-side code.

Leaderboard object #

Each leaderboard is a collection of records where each record is a ranked score with metadata. A leaderboard is uniquely identified by an ID.

Leaderboard records are sorted based on their configured sort order: DESC (default) or ASC. The sort order is decided when a leaderboard is created and cannot be changed later.

Operators #

Configure how leaderboard record scores are set using the following operator values:

  • set - The value will be set to the submitted score
  • best - The value will only be updated if the score being submitted is higher than the existing score
  • incr - The value will be incremented by the score being submitted to it
  • decr - The value will be decreased by the score being submitted to it

The operator cannot be changed after the leaderboard is created. All leaderboard configuration is immutable once created. You should delete the leaderboard and create a new one if you need to change the sort order or operator.

You can implement arcade-style leaderboards - where a single user posts multiple entries - by setting the entry’s id to a combination of the user’s ID and a current Unix time (e.g., <user-id>:<unix-time>).

Reset schedules #

You can assign each leaderboard an optional reset schedule. Records contained in the leaderboard will expire based on this schedule and users will be able to submit new scores for each reset cycle. At the expiry of each reset period the server triggers callbacks with the current leaderboard state. Read about about it Leaderboards Best Practices and see the Tiered Leagues guide for an example use case.

Reset schedules are defined in CRON format when the leaderboard is created. If a leaderboard has no reset schedule set its records will never expire.

Leaderboard records #

Each leaderboard contains a list of records ordered by their scores.

All records belong to an owner. This is usually a user but other objects like a group ID or some other custom ID can be used. Each owner will only have one record per leaderboard. If a leaderboard expires each owner will be able to submit a new score which rolls over.

The score in each record can be updated as the owner progresses. Scores can be updated as often as wanted and can increase or decrease depending on the combination of leaderboard sort order and operator.

Client
1
2
3
curl -X POST "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
  -d '{"record": {"score": 100}}'
Client
1
2
3
4
var leaderboardId = "level1";
var submission = {score: 100};
var record = await client.writeLeaderboardRecord(session, leaderboardId, submission);
console.log("New record username %o and score %o", record.username, record.score);
Client
1
2
3
4
const string leaderboardId = "level1";
const long score = 100L;
var r = await client.WriteLeaderboardRecordAsync(session, leaderboardId, score);
System.Console.WriteLine("New record for '{0}' score '{1}'", r.Username, r.Score);
Client
1
2
3
4
5
6
7
8
9
auto successCallback = [](const NLeaderboardRecord& record)
{
    std::cout << "New record with score " << record.score << std::endl;
};

string leaderboardId = "level1";
int64_t score = 100;

client->writeLeaderboardRecord(session, leaderboardId, score, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
final String leaderboard = "level1";
long score = 100L;
LeaderboardRecord r = client.writeLeaderboardRecord(session, leaderboard, score);
System.out.format("New record for %s score %s", r.getUsername(), r.getScore());
Client
1
2
3
4
5
6
7
var leaderboard_id = "level1"
var score = 100
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, leaderboard_id, score), "completed")
if record.is_exception():
    print("An error occurred: %s" % record)
    return
print("New record username %s and score %s" % [record.username, record.score])
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
POST /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "record": {
    "score": 100
  }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
local leaderboard_id = "level1"
local score = 100
local subscore = 0
local metadata = json.encode({ weather_conditions = "rain" })
local operator = "best"

local result = client.write_leaderboard_record(leaderboard_id, metadata, operator, score, subscore)
if result.error then
  print(result.message)
  return
end

Records metadata #

Each record can optionally include additional data about the score or the owner when submitted. The extra fields must be JSON encoded and submitted as the metadata.

An example use case for metadata is info about race conditions in a driving game, such as weather, which can give extra UI hints when users list scores.

1
2
3
4
5
{
  "surface": "wet",
  "timeOfDay": "night",
  "car": "Porsche 918 Spyder"
}

Create a leaderboard #

A leaderboard can be created via server-side code at startup or within a registered function. The ID given to the leaderboard is used to submit scores to it.

Server
1
2
3
4
5
6
7
8
9
local id = "level1"
local authoritative = false
local sort = "desc"
local operator = "best"
local reset = "0 0 * * 1"
local metadata = {
  weather_conditions = "rain"
}
nk.leaderboard_create(id, authoritative, sort, operator, reset, metadata)
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
id := "level1"
authoritative := false
sort := "desc"
operator := "best"
reset := "0 0 * * 1"
metadata := map[string]interface{}{"weather_conditions": "rain"}

if err := nk.LeaderboardCreate(ctx, id, authoritative, sort, operator, reset, metadata); err != nil {
    // Handle error.
}
Server
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let id = '4ec4f126-3f9d-11e7-84ef-b7c182b36521';
let authoritative = false;
let sort = nkruntime.SortOrder.DESCENDING;
let operator = nkruntime.Operator.BEST;
let reset = '0 0 * * 1';
let metadata = {
  weatherConditions: 'rain',
};
try {
    nk.leaderboardCreate(id, authoritative, sort, operator, reset, metadata);
} catch(error) {
    // Handle error
}

Submit a score #

A user can submit a score to a leaderboard and update it at any time. When a score is submitted the leaderboard’s pre-configured sort order and operator determine what effect the operation will have.

The “set” operator will ensure the leaderboard record always keeps the latest value, even if it is worse than the previous one.

Submitting to a leaderboard with the “best” operator ensures the record tracks the best value it has seen for that record. For a descending leaderboard this means the highest value, for an ascending one the lowest. If there is no previous value for the record, this behaves like “set”.

With the “incr” operator the new value is added to any existing score for that record. If there is no previous value for the record, this behaves like “set”.

Client
1
2
3
curl -X POST "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
  -d '{"score": 100}'
Client
1
2
3
4
var leaderboardId = "level1";
var submission = {score: 100};
var record = await client.writeLeaderboardRecord(session, leaderboardId, submission);
console.log("New record username %o and score %o", record.username, record.score);
Client
1
2
3
4
const string leaderboard = "level1";
const long score = 100L;
var r = await client.WriteLeaderboardRecordAsync(session, leaderboard, score);
System.Console.WriteLine("New record for '{0}' score '{1}'", r.Username, r.Score);
Client
1
2
3
4
5
6
7
8
9
auto successCallback = [](const NLeaderboardRecord& record)
{
    std::cout << "New record with score " << record.score << std::endl;
};

string leaderboardId = "level1";
int64_t score = 100;

client->writeLeaderboardRecord(session, leaderboardId, score, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
final String leaderboard = "level1";
long score = 100L;
LeaderboardRecord r = client.writeLeaderboardRecord(session, leaderboard, score);
System.out.format("New record for %s score %d", r.getUsername(), r.getScore());
Client
1
2
3
4
5
6
7
var leaderboard_id = "level1"
var score = 100
var record : NakamaAPI.ApiLeaderboardRecord = yield(client.write_leaderboard_record_async(session, leaderboard_id, score), "completed")
if record.is_exception():
    print("An error occurred: %s" % record)
    return
print("New record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
6
7
8
9
POST /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>

{
  "score": 100
}
Client
1
2
3
4
5
6
7
8
9
local leaderboard_id = "level1"
local score = 100
local operator = "best"

local result = client.write_leaderboard_record(leaderboard_id, nil, operator, score)
if result.error then
  print(result.message)
  return
end

List records #

A user can list records from a leaderboard. This makes it easy to compare scores to other users and see their positions.

List by score #

The standard way to list records is ordered by score based on the sort order in the leaderboard.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
var leaderboardId = "level1";
var result = await client.listLeaderboardRecords(session, leaderboardId);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
5
6
const string leaderboardId = "level1";
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

string leaderboardId = "level1";

client->listLeaderboardRecords(session, leaderboardId, {}, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
final String leaderboard = "level1";
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
var leaderboard_id = "level1"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
1
2
3
4
5
6
7
8
9
local leaderboard_id = "level1"
local result = client.list_leaderboard_records(leaderboard_id)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

You can fetch the next set of results with a cursor.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?cursor=<next_cursor>" \
  -H 'Authorization: Bearer <session token>'
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
var leaderboardId = "level1";

var result = await client.listLeaderboardRecords(session, leaderboardId);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});

// If there are more results get next page.
if (result.next_cursor) {
  result = await client.listLeaderboardRecords(session, leaderboardId, null, null, result.next_cursor);
  result.records.forEach(function(record) {
    console.log("Record username %o and score %o", record.username, record.score);
  });
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const string leaderboardId = "level1";
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
// If there are more results get next page.
if (result.NextCursor != null)
{
    var c = result.NextCursor;
    result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, null, 100, c);
    foreach (var r in result.Records)
    {
        System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
auto successCallback = [this](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }

    if (!recordsList->nextCursor.empty())
    {
        auto successCallback = [this](NLeaderboardRecordListPtr recordsList)
        {
            for (auto& record : recordsList->records)
            {
                std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
            }
        };

        string leaderboardId = "level1";

        client->listLeaderboardRecords(session, leaderboardId, {}, opt::nullopt, recordsList->nextCursor, successCallback);
    }
};

string leaderboardId = "level1";

client->listLeaderboardRecords(session,
    leaderboardId,
    {},
    opt::nullopt,
    opt::nullopt,
    successCallback
);
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
final String leaderboard = "level1";
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}

// If there are more results get next page.
if (records.getCursor() != null) {
    var c = result.NextCursor;
    records = client.listLeaderboardRecords(session, leaderboard, null, 100, records.getNextCursor());
    for (LeaderboardRecord record : records.getRecordsList()) {
        System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
    }
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
var leaderboard_id = "level1"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, null, null, 100), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])

if result.next_cursor:
    result = yield(client.list_leaderboard_records_async(session, leaderboard_id, null, null, 100, result.next_cursor), "completed")
    if result.is_exception():
        print("An error occurred: %s" % result)
        return
    for r in result.records:
        var record : NakamaAPI.ApiLeaderboardRecord = r
        print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?cursor=<next_cursor>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
local leaderboard_id = "level1"
local cursor = nil
local owner_id = nil
local limit = 100
repeat
  local result = client.list_leaderboard_records(leaderboard_id, owner_id, limit, cursor)
  if result.error then
    print(result.message)
    return
  end
  for _,record in ipair(result.records) do
    pprint(record)
  end
until not cursor

List by friends #

You can use a bunch of owner IDs to filter the records to only ones owned by those users. This can be used to retrieve only scores belonging to the user’s friends.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?owner_ids=some&owner_ids=friends" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var leaderboardId = "level1";
var ownerIds = ["some", "friends", "user ids"];
var result = await client.listLeaderboardRecords(session, leaderboardId, ownerIds);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o", record.username, record.score);
});
Client
1
2
3
4
5
6
7
const string leaderboardId = "level1";
var ownerIds = new[] {"some", "friends", "user ids"};
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, ownerIds);
foreach (var r in result.OwnerRecords)
{
    System.Console.WriteLine("Record for '{0}' score '{1}'", r.Username, r.Score);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << std::endl;
    }
};

vector<string> ownerIds = { "some", "friends", "user ids" };
string leaderboardId = "level1";

client->listLeaderboardRecords(session, leaderboardId, ownerIds, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
String leaderboard = "level1";
String[] ownerIds = new String[] {"some", "friends", "user ids"};
LeaderboardRecordList records = await client.ListLeaderboardRecordsAsync(session, leaderboard, ownerIds);
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "level1"
var owner_ids = ["some", "friend", "user id"]
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, owner_ids), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?owner_ids=some&owner_ids=friends
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "level1"
local owner_ids = { "some", "friend", "user id" }
local result = client.list_leaderboard_records(leaderboard_id, owner_ids)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

List expired records #

Expired records after each leaderboard reset are removed from the leaderboard rankings but they are not deleted. A user can list expired records for their desired time period.

For example, to list records expired in the past week:

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboardId>?overrideExpiry=604800" \
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
5
6
var leaderboardId = "<leaderboardId>";
var overrideExpiry = 604800;
var result = await client.listLeaderboardRecords(session, leaderboardId, overrideExpiry);
result.records.forEach(function(record) {
  console.log("Record username %o and score %o expired on %o", record.username, record.score, record.expiryTime);
});
Client
1
2
3
4
5
6
7
const string leaderboardId = "<leaderboardId>";
const int64 overrideExpiry = 604800;
var result = await client.ListLeaderboardRecordsAsync(session, leaderboardId, overrideExpiry);
foreach (var r in result.Records)
{
    System.Console.WriteLine("Record for '{0}' and score '{1}' expired on '{2}'", r.Username, r.Score, r.ExpiryTime);
}
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
auto successCallback = [](NLeaderboardRecordListPtr recordsList)
{
    for (auto& record : recordsList->records)
    {
        std::cout << "Record username " << record.username << " and score " << record.score << " expired on " << record.expiryTime << std::endl;
    }
};

string leaderboardId = "<leaderboardId>";
int overrideExpiry = 604800;

client->listLeaderboardRecords(session, leaderboardId, overrideExpiry, {}, opt::nullopt, opt::nullopt, successCallback);
Client
1
2
3
4
5
6
final String leaderboard = "<leaderboardId>";
final Long overrideExpiry = 604800;
LeaderboardRecordList records = client.listLeaderboardRecords(session, leaderboard, overrideExpiry).get();
for (LeaderboardRecord record : records.getRecordsList()) {
    System.out.format("Record for %s score %d", record.getUsername(), record.getScore());
}
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "<leaderboardId>"
var override_expiry = 604800
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_async(session, leaderboard_id, override_expiry), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s expired on %s" % [record.username, record.score, record.expiry_time])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboardId>?overrideexpiry=604800
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "<leaderboardId>"
local override_expiry = 604800
local result = client.list_leaderboard_records(leaderboard_id, owner_ids, limit, cursor, override_expiry)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end

List leaderboard records around owner #

Fetch the list of leaderboard records around the owner.

Client
1
2
curl -X GET "http://127.0.0.1:7350/v2/leaderboard/<leaderboard_id>/owner/<owner_id>?limit=<limit>"
  -H 'Authorization: Bearer <session token>'
Client
1
2
3
4
var id = "someid";
var ownerId = "some user ID";
var limit = 100;
var result = await client.listLeaderboardRecordsAroundOwner(session, id, ownerId, limit);
Client
1
2
3
4
var leaderboardId = "someid";
var ownerId = session.UserId;
var limit = 100;
var result = await client.ListLeaderboardRecordsAroundOwnerAsync(session, leaderboardId, ownerId, limit);
Client
1
2
3
4
string leaderboardId = "level1";
string ownerId = "some user ID";
int32_t limit = 100;
client->listLeaderboardRecordsAroundOwner(session, leaderboardId, ownerId, limit, successCallback);
Client
1
2
3
4
String id = "someid";
String ownerId = session.getUserId();
int limit = 100;
LeaderboardRecordList records = client.listLeaderboardRecordsAroundOwner(session, id, ownerId, limit).get();
Client
1
2
3
4
5
6
7
8
9
var leaderboard_id = "level1"
var owner_id = "user id"
var result : NakamaAPI.ApiLeaderboardRecordList = yield(client.list_leaderboard_records_around_owner_async(session, leaderboard_id, owner_id), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for r in result.records:
    var record : NakamaAPI.ApiLeaderboardRecord = r
    print("Record username %s and score %s" % [record.username, record.score])
Client
1
2
3
4
5
GET /v2/leaderboard/<leaderboard_id>/owner/<owner_id>?limit=<limit>
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
Client
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
local leaderboard_id = "level1"
local owner_id = "user id"
local result = client.list_leaderboard_records_around_owner(leaderboard_id, owner_id)
if result.error then
  print(result.message)
  return
end
for _,record in ipair(result.records) do
  pprint(record)
end