Sign in

Challenges

SCILL challenges are a simple, yet effective method of retaining users in your platform by offering them something to do and to rewarding them for doing so. In games, sports and even business, challenges offer an interesting and engrossing, yet inconspicuous way for users to leave their comfort zone.

While challenges are a simple concept, in practice it’s not easy to implement them. You need to track massive amounts of data, must process it, and in most cases GDPR compliances are mandatory.

Leverage SCILL’s easy-to-use backends to send event data, build challenges visually with our challenge creator in the Admin Panel and implement them in a couple of hours into your existing application or game.

Base URL

https://pcs.scillgame.com

Please note

Challenges have a lifetime cycle that is reflected in the type attribute of the Challenge object.

  1. unlock
  2. unlocked
  3. in-progress
  4. unclaimed
  5. finished

Every challenge is unlock by default. Use the Unlock challenge endpoint to unlock the challenge. You might want to sell some challenges, which is why they need to be unlocked first.

Unlocked challenges need to be activated. This can be done with the Activate challenge endpoint. Only activated challenges track progress. Activated challenges have a lifetime until they time-out. You can set the challenge_duration_time in the Admin Panel.

Challenge Life Cycle

Challenges have a life cycle. They need to be activated, then “wait” for events coming in driving the challenges counter or until the challenge has been expired. Then, they either get “recreated” if they are repeatable or are removed for the specific user.

The life cycle is a bit different if the challenge has manual or automatic time management.

Please note

Challenges have a lifetime cycle that is reflected in the type attribute of the Challenge object.

  1. unlock
  2. unlocked
  3. in-progress
  4. [lost]
  5. unclaimed
  6. finished

The lost type is only used in notifications via MQTT (realtime updates) or Webhooks. You will not receive this type via the get[All|Unresolved]PersonalChallenges routes.

Manual challenges

Every challenge is unlock by default. Use the Unlock challenge endpoint to unlock the challenge. You might want to sell some challenges, which is why they need to be unlocked first.

Unlocked challenges need to be activated. This can be done with the Activate challenge endpoint. Only activated challenges track progress. Activated challenges have a lifetime until they time-out. You can set the challenge_duration_time in the Admin Panel.

Automatic challenges

Automatic challenges are activated and handled automatically by the SCILL backend. You can define this in the Admin Panel. The process is automated and no script or user action has to be done to drive the challenge through it’s life cycle other than sending events.

Manual Challenges

As you can see, manual challenges require user action at most steps. They need to be manually unlocked and activated by the user or a script on your side, and they also need to be manually claimed (i.e. collecting the reward).

The use case for this is for challenges that require and bind the user attention for a short period of time with a very specific target in mind. Some example usages for that:

Cancel two unnecessary subscriptions today. This is something that requires some attention and can only be solved by completing a couple of steps: Check out back account, identify if you still need that subscription and you finally need to login into the account, perhaps before reset the password and then finding that “Cancel” button. Perhaps you need to call a call center. You have this in mind every day, but you postpone it day by day. So, an automatic challenge does not make much sense, because it would be available every day again.

But, having a manual challenges with a nice reward for canceling the subscription (a voucher for a nice bottle of wine), will be enough: The user activates the challenges and then has 24 hours time to resolve the challenge. No excuses, and if the challenge runs out, it’s gone - as is the reward.

Automatic Challenges

These challenges are more standard challenges. Challenges set to be automatic will be automatically unlocked and activated as soon as they are requested from the backend for the user provided with the access token.

These challenges listen on incoming events and will be reset every day, week or month (or never) at a specific time and in a specific time zone (often midnight).

Example for this is: Make 10.000 steps every day. This challenge would be an automatic challenge, set to be repeatable, and being reset every day and at 00:00:00.

Another example would be: Buy flowers for your wife on valentines day. In this case, you would set the challenge to not being repeatable, because it’s a one-time challenge. You would use the live_date setting to be February 14th midnight, and the challenge would last a day and would be reset at 00:00:00. This way, the challenge would pop up in your app on February 14th and would also be activated automatically.

The Challenge object

Challenge objects allow you to quickly build challenges into your application.

Users need to be aware of active challenges, as they typically have to execute certain steps in order to complete a challenge in time. Therefore, by default, challenges are locked. That means, that challenges don’t track any progress and events are not being processed by the backend even if it would make sense to process them in the context of a challenge.

You need to unlock a specific challenge for a specific user. You can either do that automatically for a user, or you can let the user choose a challenge to process next. That depends on your application.

Properties

challenge_id string

The unique id of this challenge. Every challenge is linked to a product.

user_challenge_id string

If this challenge is unlocked (i.e. active see type) then this is the unique id of the challenge assiociated to the user. Otherwise this is 0 or empty.

challenge_name string

The name of the challenge in the language set by the language parameter.

challenge_description string

An optional multi-language description that can be set in the Admin Panel. Used to describe exactly what the user has to do.

challenge_duration_time integer

The duration of the challenge in seconds. Challenges auto lock after time-out and need to be unlocked again. Challenges that don’t time out have a challenge_duration_time of -1! If you have a “Time remaining” kind of UI, you should only display that if challenge_duration_time > 0!

live_date string

The date this challenge should start. Use that field to create challenges that start in the future.

challenge_goal integer

Indicates how many “tasks” must be completed or done to complete this challenge.

challenge_goal_condition integer

With this you can set the way how the SCILL system approaches the challenges state. 0 means, that the counter of the challenge must be brought above the goal. If this is 1, then the counter must be kept below the goal. This is often useful for challenges that include times, like: Manage the level in under 50 seconds.

challenge_current_score integer

Indicates how many “tasks” the user already has completed. Use this in combination with challenge_goal to render a nice progress bar.

challenge_price integer

You can set this value in the Admin Panel. You can use that to set a price tag or a price identifier for your own backend.

challenge_reward string

Set a reward for this challenge. This is a string value that you can map to anything in your code. Use in combination with challenge_reward_type.

challenge_reward_type string

The reward type can be set to various different settings. Use it to implement different reward types on your side and use challenge_reward to set the value or amount of this reward.

challenge_xp integer

Many games have some sort of experience points or levels. You can use that integer to set the increment in these points.

challenge_icon string

In the admin panel you can set a string representing an image. This can be a URL, but it can also be an image or texture that you have in your games asset database.

challenge_icon_hd string OPTIONAL

This is the HD variant of the challenge icon image. If you have a game, that runs on multiple platforms that could come in handy. Otherwise just leave blank.

repeatable boolean

If this challenge can be only activated once per user this will be false. Otherwise this challenge will always be added to list of available challenges (see personal or alliance challenges).

type string

Indicates the status of the challenge. This can be:

unlock
Challenge does not track anything.
unlocked
Challenge is unlocked but needs to be activated to start tracking progress
in-progress
Challenge is active and tracking
unclaimed
The challenge has been completed but the reward has not yet been claimed
finished
The challenge has been successfully be completed and the reward has been claimed

Only in Webhook payloads you will also find these types. In your User Interface you don’t need to implement them, as you will not get these states in the request APIs.

lost
The challenge has been lost (i.e. timed out without having 100% progress achieved). This type will only be sent in payloads.

is_claimed boolean

If the challenge reward has been claimed this is true otherwise its false.

user_challenge_unlocked_at date

This is the timestamp the challenge has been unlocked.

user_challenge_activated_at date

This is the timestamp the challenge is activated. Use it in combination with challenge_duration_time to calculate the time the challenge will expire.

Info

Please note: We will charge you for unlocked challenges only. Adding challenges via the Admin Panel will not cost you anything - but each challenge that you activate automatically or on user action will add to the number of challenges requested per month!

The Challenge Object
{
  "challenge_id": "505538946732425217",
  "challenge_name": "Survive 3 battles",
  "challenge_duration_time": 500,
  "live_date": "2020-10-12T00:00:00Z",
  "challenge_goal": 5,
  "user_challenge_current_score": 0,
  "challenge_icon": "black-arrow",
  "challenge_icon_hd": "black-arrow-hd",
  "challenge_price": 0,
  "challenge_reward": "weapon_a",
  "challenge_reward_type": "item",
  "challenge_goal_condition": 0,
  "challenge_xp": 0,
  "repeatable": false,
  "type": "unlock",
  "is_claimed": false,
  "user_challenge_unlocked_at": null,
  "user_challenge_activated_at": null,
  "user_challenge_is_claimed": false,
  "user_challenge_status": 0
}
Calculating remaining time
let activatedAt = new Date(challenge.user_challenge_activated_at);
// Using moment it's easy to generate a new date:
let activeTill = moment(activatedAt).add(this.duration, 'm').toDate();
// Using countdown it's also easy to extract in realtime the time left
const timeRemaining = countdown(new Date(), this.targetDate);
var timeText = "";
var date = DateTime.Parse(challenge.user_challenge_activated_at);
date = date.AddMinutes((double)challenge.challenge_duration_time);

var now = DateTime.Now;
var diff = date.Subtract(now);

if (diff.Days > 0)
{
    timeText = "+24 hours";
}
else
{
    timeText = String.Format("{0:00}:{1:00}:{2:00}", diff.Hours, diff.Minutes, diff.Seconds);
}

The ChallengeCategory object

SCILL challenges are organized into categories. A maximum of 1.000 challenges can be added per category. You setup these categories and challenges in the Admin Panel.

ChallengeCategory objects build structures of different challenges the user can choose from. Of course you can also just show specific categories to the user or you just show a specific challenge to the user.

Properties

is_daily_category boolean

Indicates if this is the daily category, bringing up new challenges every day for the user to tackle.

category_position integer

In the admin panel you set the order of the categories. This is the position index and indicates the position within the categories array.

category_slug string

A short name without special chars to make it easier to refer to a specific category (in code) that is language and id agnostic.

category_name string

The name of the category in the local language set as the query parameter.

category_id string

The unique id of this category.

challenges Challenge

An array of Challenge objects.

Challenge Categories
{
  "is_daily_category": false,
  "category_position": 0,
  "category_slug": "beginner-challenges",
  "category_name": "Beginner challenges",
  "category_id": "543133912041586700",
  "challenges": [
      {
      "challenge_id": "505538946732425217",
      "challenge_name": "Survive 3 battles",
      "challenge_duration_time": 500,
      "live_date": "2020-10-12T00:00:00Z",
      "challenge_goal": 5,
      "user_challenge_current_score": 0,
      "challenge_icon": "black-arrow",
      "challenge_icon_hd": "black-arrow-hd",
      "challenge_price": 0,
      "challenge_reward": "weapon_a",
      "challenge_reward_type": "item",
      "challenge_goal_condition": 0,
      "challenge_xp": 0,
      "repeatable": false,
      "type": "unlock",
      "is_claimed": false,
      "user_challenge_unlocked_at": null,
      "user_challenge_activated_at": null,
      "user_challenge_is_claimed": false,
      "user_challenge_status": 0
    }
  ]
}

SCILL challenges are organized into categories. A maximum of 1.000 challenges can be added per category. You setup these categories and challenges in the Admin Panel.

Request challenges

Use these endpoints to request personal challenges on behalf of a user. This endpoint will deliver all challenges you have setup in the Admin Panel for a specific user.

Warning

This endpoints requires an access token that you need to create for the user. Please consult Authentication for more info about this topic.

Get All Challenges

URL api/v1/challenges/personal/all/:pid
Method GET
Authentication Access Token
Path parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

Use this endpoint to get all available challenges for your product. This endpoint will also return finished challenges. This will be personalized for the user encoded in the access token.

Tip

In applications that show a to do like list it often makes sense to also show completed challenges (with a checked box or striked through). Use this route for these kind of applications.

Get All Unresolved Challenges

URL api/v1/challenges/personal/unresolved/:pid
Method GET
Authentication Access Token
Path parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

Use this endpoint to get all available challenges that are not finished yet, i.e. those challenges that have not been finished yet.

Tip

In applications that allow users to pick a couple of challenges it often makes sense not to show completed challenges. Use this route for these kind of applications.

Get All Active Challenges

URL api/v1/challenges/personal/get-in-progress-challenges/:pid
Method GET
Authentication Access Token
Path parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

Get all active challenges (i.e. type = in-progress) for your product and the user encoded in the access token. This will also be separated in categories as shown below.

Getting personal challenges
const scill = require('@scillgame/scill-js');

// Please see Authentication API reference on how to get an access token
const accessToken = getAccessToken();

const challengesApi = scill.getChallengesApi(accessToken);
challengesApi.getAllPersonalChallenges('__YOUR_APP_ID__').then(categories => {
  categories.forEach((category => {
    category.challenges.forEach((challenge => {
      console.log(challenge);
    }));
  }));
});
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

var categories = await scillClient.GetPersonalChallengesAsync();
foreach (var category in categories)
{
	// Categories have challenges
    foreach (var challenge in category.challenges)
    {
        // Challenge objects
    }
}
The Personal Challenges Response
[
  {
    "is_daily_category": false,
    "category_position": 0,
    "category_slug": "beginner-challenges",
    "category_name": "Beginner challenges",
    "category_id": "543133912041586700",
    "challenges": [
          {
        "challenge_id": "505538946732425217",
        "challenge_name": "Survive 3 battles",
        "challenge_duration_time": 500,
        "live_date": "2020-10-12T00:00:00Z",
        "challenge_goal": 5,
        "user_challenge_current_score": 0,
        "challenge_icon": "black-arrow",
        "challenge_icon_hd": "black-arrow-hd",
        "challenge_price": 0,
        "challenge_reward": "weapon_a",
        "challenge_reward_type": "item",
        "challenge_goal_condition": 0,
        "challenge_xp": 0,
        "repeatable": false,
        "type": "unlock",
        "is_claimed": false,
        "user_challenge_unlocked_at": null,
        "user_challenge_activated_at": null,
        "user_challenge_is_claimed": false,
        "user_challenge_status": 0
      }
    ]
  }
]

Unlock a challenge

Challenges are locked by default and don’t track any progress. Unlock a challenge using this REST-API endpoint.

URL api/v1/challenges/personal/unlock/:pid/:cid
Method GET
Authentication Access Token
Path parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

cid integer REQUIRED

The unique challenge id. This is challenge_id in the Challenge object.

Unlock personal challenges
const scill = require('@scillgame/scill-js');

// Please see Authentication API reference on how to get an access token
const accessToken = getAccessToken();
const challengesApi = scill.getChallengesApi(accessToken);

// Get the challengeId from challenge.challenge_id for example
challengesApi.unlockChallenge("__YOUR_APP_ID__", challengeId).then((response => {
  if (response.status < 300) {
    // Success, response contains updated challenge data in challenge property
    const updatedChallenge = response.challenge;
    // Use that to override your local challenge instance
  }
}));
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

// Get the challengeId from challenge.challenge_id for example
var response = scillClient.UnlockPersonalChallenge(challengeId);
if (response.status < 300)
{
	// Success, response.challenge contains the updated challenge object
	// Use that to update your local data to refesh UI
    if (response.challenge != null)
    {
        UpdateChallenge(response.challenge);
    }
}
The Unlock Challenge Response
{
  "status": 200,
  "message": "OK",
  "challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "unlocked",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  }
}

Activate a challenge

Challenges must be activated before they will track progress. Please note, that this endpoint requires the user_challenge_id set as the second paramater.

URL api/v1/challenges/personal/activate/:pid/:cid
Method GET
Authentication Access Token
Parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

cid integer REQUIRED

The unique challenge id. This is challenge_id in the Challenge object.

Activate personal challenges
const scill = require('@scillgame/scill-js');

// Please see Authentication API reference on how to get an access token
const accessToken = getAccessToken();
const challengesApi = scill.getChallengesApi(accessToken);

// Get the challengeId from challenge.challenge_id for example
challengesApi.activateChallenge("__YOUR_APP_ID__", challengeId).then((response => {
  if (response.status < 300) {
    // Success, response contains updated challenge data in challenge property
    const updatedChallenge = response.challenge;
    // Use that to override your local challenge instance
  }
}));
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

// Get the challengeId from challenge.challenge_id for example
var response = scillClient.ActivatePersonalChallenge(challengeId);
if (response.status < 300)
{
	// Success, response.challenge contains the updated challenge object
	// Use that to update your local data to refesh UI
    if (response.challenge != null)
    {
        UpdateChallenge(response.challenge);
    }
}
The Activate Challenge Response
{
  "status": 200,
  "message": "OK",
  "challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "in-progress",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  }
}

Cancel a challenge

Challenges can be canceled at any time. Use this endpoint to cancel a challenge.

URL api/v1/challenges/personal/cancel/:pid/:cid
Method DELETE
Authentication Access Token
Parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

cid integer REQUIRED

The unique challenge id. This is challenge_id in the Challenge object.

Please note

Please note: If you cancel a challenge, that is not repeatable it will not be available for this user again. If it’s set to repeatable, then the challenge will be available in unlock type in the next load. Please, after canceling a challenge, request available challenges again and update your UI. There is no way (right now) to handle that on client side as we don’t expose that repeatable setting on client side and we don’t send notifications.

A challenge will have type cancelled after it has been canceled and there will also be send a notication where type changes from in-progress to cancelled. If that is the case, just reload the challenges from the server.

Cancel personal challenges
const scill = require('@scillgame/scill-js');

// Please see Authentication API reference on how to get an access token
const accessToken = getAccessToken();
const challengesApi = scill.getChallengesApi(accessToken);

// Get the challengeId from challenge.challenge_id for example
challengesApi.cancelChallenge("__YOUR_APP_ID__", challengeId).then((response => {
  if (response.status < 300) {
    // Success, response contains updated challenge data in challenge property
    const updatedChallenge = response.challenge;
    // Use that to override your local challenge instance
  }
}));
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

// Get the challengeId from challenge.challenge_id for example
var response = scillClient.CancelPersonalChallenge(challengeId);
if (response.status < 300)
{
	// Success, response.challenge contains the updated challenge object
	// Use that to update your local data to refesh UI
    if (response.challenge != null)
    {
        UpdateChallenge(response.challenge);
    }
}
The Cancel Challenge Response
{
  "status": 200,
  "message": "OK",
  "challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "unlock",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  }
}

Claim a reward

Completed challenges must be claimed to finish them. Either you do that automatically (for the user) or you offer a user interface for the user to claim the challenge. Only challenges where type is unclaimed can be claimed. Otherwise this request will fail.

URL api/v1/challenges/personal/claim/:pid/:cid
Method GET
Authentication Access Token
Parameters

pid integer REQUIRED

The unique id of the product. The product ID is listed in the Admin Panel.

cid integer REQUIRED

The unique challenge id. This is challenge_id in the Challenge object.

Claim personal challenge reward
const scill = require('@scillgame/scill-js');

// Please see Authentication API reference on how to get an access token
const accessToken = getAccessToken();
const challengesApi = scill.getChallengesApi(accessToken);

// Get the challengeId from challenge.challenge_id for example
challengesApi.claimPersonalChallengeReward("__YOUR_APP_ID__", challengeId).then((response => {
  if (response.status < 300) {
    // Success, response contains updated challenge data in challenge property
    const updatedChallenge = response.challenge;
    // Use that to override your local challenge instance
  }
}));
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

// Get the challengeId from challenge.challenge_id for example
var response = scillClient.ClaimPersonalChallengeReward(challengeId);
if (response.status < 300)
{
	// Success, response.challenge contains the updated challenge object
	// Use that to update your local data to refesh UI
    if (response.challenge != null)
    {
        UpdateChallenge(response.challenge);
    }
}
The Claim Challenge Response
{
  "status": 200,
  "message": "OK",
  "challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "unlock",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  }
}

Realtime updates

Getting challenge status updates in realtime is important for a user. Therefore SCILL provides interfaces you can subscribe to in order to get updates in realtime. This is realized via MQTT.

Tip

Using our SDKs is the simplest way to get realtime updates. If you are curious to understand how this is implemented behind the scenes, check out the Realtime updates document.

1. Get a topic

First, you need to request an topic for our MQTT server. A topic is a channel that you subscribe to get messages that are published into that channel. Our backend publishes payloads of challenge updates into these topics that you can respond to in your code.

URL api/v1/auth/user-challenges-topic-link
Method GET
Authentication Access Token
The Generate Topic Response
{
  "topic":"topic/challenges/2af452ba...."
}

2. Connect to the MQTT server

Second, you need to connect to our MQTT server either with the MQTT protocol or the Websocket protocol in browser environments:

Protocol URL
MQTT mqtt://mqtt.scillgame.com:1883
Websocket ws://mqtt.scillgame.com:8083/mqtt

After connection is established, subscribe to the topic generated earlier. You’ll get ChallengeWebhookPayload objects that you can use to update UI or implement business logic.

You can either “overwrite” the data you previously stored after calling the Get all active challenges request or you can just use this web socket event to reload the data using the REST-API.

Tip

Please note: Use our SDKs to subscribe to realtime updates. We do all heavy lifting and keep those SDKs updated to the latest versions. We also make sure to close connections if they are not needed.

Use SDK to get notifications
const SCILL = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const monitor = SCILL.startMonitorChallengeUpdates(accessToken, (payload) => {
  // payload is of type ChallengeWebhookPayload and contains the challenge in the new and the old state
  // use it to update existing UI
});
// Please read Authentication documentation on how to get an access token
var scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");

// Request notifications for challenges
scillClient.StartChallengeUpdateNotifications(payload => {
	// You get a payload of type ChallengeWebhookPayload
});
Manually subscribe to MQTT server
// Please note, in this example we use the MQTT.js JavaScript library
const scill = require('@scillgame/scill-js');
const mqtt = require('mqtt');

// Implemented earlier - get an access token for the current user
const accessToken = getAccessToken();

// Get the MQTT topic and subscribe to the SCILL MQTT server
const authApi = scill.getAuthApi(accessToken, environment);
authApi.getUserChallengesNotificationTopic().then(notificationTopic => {
  console.log("Received notification topic", notificationTopic);

  const challengesClient = mqtt.connect('wss://mqtt.scillgame.com:8083/mqtt');
  this.challengeClient = challengesClient;

  challengesClient.on('connect', function () {
    challengesClient.subscribe(notificationTopic.topic, function (err) {
      if (err) {
        console.warn("Subscribing to MQTT failed");
      }
    })
  })

  challengesClient.on('message', function (topic, message) {
    if (message && message.length > 0) {
      try {
        var payload = JSON.parse(message.toString("utf8"));
        if (payload) {
          handler(payload);
        }
      } catch (e) {
        console.warn("MQTT payload could not be parsed", topic, message.toString("utf8"));
      }
    }
  });
});
// Requires MQTTnet package

public class ExampleClass
{
	private SCILLClient _scillClient;
	private IMqttClient _challengesMqttClient;

    public delegate void ChallengeChangedNotificationHandler(ChallengeWebhookPayload payload);
    public event ChallengeChangedNotificationHandler OnChallengeChangedNotification;

    public ExampleClass(string accessToken)
    {
        _scillClient = new SCILLClient(accessToken, "__YOUR_APP_ID__");
    }

    ~ExampleClass()
    {
        StopMonitoring();
    }

    private IMqttClient CreateMQTTClient()
    {
        var client = _mqttFactory.CreateMqttClient();
        _mqttClients.Add(client);
        return client;
    }

    public async void StartMonitorUserChallenges()
    {
        _challengesMqttClient = CreateMQTTClient();

        // Subscribe to that topic once the MQTT connection is established
        _challengesMqttClient.UseConnectedHandler(async e =>
        {
            // Get the MQTT topic for listening on changes for the challenges
            var notificationTopic = await _scillClient.AuthApi.GetUserChallengesNotificationTopicAsync();

            // Subscribe to a topic
            await _challengesMqttClient.SubscribeAsync(new MqttTopicFilterBuilder()
                .WithTopic(notificationTopic.topic).Build());
        });

        // Handle incoming messages and send payloads to callback handler
        _challengesMqttClient.UseApplicationMessageReceivedHandler(e =>
        {
            string jsonStr = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
            var payload = JsonConvert.DeserializeObject<ChallengeWebhookPayload>(jsonStr);
            if (payload != null)
            {
                OnChallengeChangedNotification?.Invoke(payload);
            }
        });

        // Connect to SCILLs MQTT server to receive real time notifications
        var options = new MqttClientOptionsBuilder()
            .WithTcpServer("mqtt.scillgame.com", 1883)
            .Build();

        await _challengesMqttClient.ConnectAsync(options, CancellationToken.None);
    }

    public async void StopMonitorUserChallenges()
    {
        if (_challengesMqttClient == null)
        {
            return;
        }

        await _challengesMqttClient.DisconnectAsync();
        _challengesMqttClient = null;
    }
}

Webhook

In the Admin Panel, you can setup a web hook that is called by our backend whenever a challenge (for a specific user) changes. This way, you can quickly add business logic on your side.

The SCILL backend will request your Webhook whenever a user activated challenge changes:

  • Webhook is called via POST
  • Your Webhook URL must be served via HTTPS and needs to have a valid certificate
  • Data is sent as application/json
  • Secret key is added to your URL via GET parameter secret_key
  • Your Webhook must return a response with HTTP status code 200.
  • If your Webhook does not return a response or with an error code (4xx, 5xx) the SCILL backend will retry sending the Webhook.
Please note

Please note: In the Admin Panel you set up a shared secret that is sent with every webhook request as GET-Parameter secret_key.

  • Check the shared secret in your web hook and return a 403 error if its not ok, otherwise return a response with HTTP code 200.
  • Keep this secret secure. It allows everyone to trigger your webhooks without permission

Data sent via POST

The data sent to your Webhook will be a JSON object with these attributes:

webhook_type string

The type of the webhook. Depending on the module, there are different webhook types indicating different events. Check the reference documentation to see all types.

category_position integer

The index of the category this challenge is linked to. When you request personal challenges, you get an array of categories which contain an array of challenges in their challenges property. This value indicates in which category this challenge can be found. Speeds up updating UI as you don’t need to iterate through all catagories and challenges to find the challenge.

old_challenge Challenge

This is the challenge before something changed as a Challenge object.

new_challenge Challenge

Delivers the current state of the challenge as a Challenge object.

You can either use the new_challenge object to update your local instance of a challenge directly, you can use the Webhook to reload the challenges via REST-API or you can also compare the old state and the new state to figure out what has changed.

Typical usage patterns are:

  • Compare user_challenge_current_score from old and new object to learn if the challenge status progressed
  • Compare type from old and new object to learn if the challenge type changed (i.e. if it got unlocked, activated, canceled or claimed)

Please note: You should always compare the old and the new value to decide what to do in your business logic. As requests are sent asyncronously your backend might not necessarily be called in the “correct” order. I.e. This is especially true for claiming a challenge. The example below shows how to implement it correctly.

Handling payloads in backend
// This example implements a NodeJS backend listening on webhook requests from SCILL to unlock items for users
const express = require('express');
const bodyParser = require('body-parser');
const scill = require('@scillgame/scill-js');

// Generate an instance of the SCILL SDK with example API-Key
const auth = scill.getAuthApi('__YOUR_API_KEY__');

const app = express();
const port = 80;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

unlockItem = function(itemId, userId) {
    // Unlock an item, for example enter a row in a database, etc.
}

// Implement a route that you can set in Admin Panel. This is what you would have to set in the admin panel:
// https://www.yourdomain.com/scill/webhook/challenges
// When creating the webhook a secret will be created by SCILL and will be sent as a GET parameter. Use the secret
// to make sure nobody can call your webhook without permission.
app.post('/scill/webhook/challenges', (req, res) => {
    const data = req.body;
    let secret = req.query.secret;

    if (!secret || secret !== "__YOUR_WEBHOOK_SECRET__") {
        return res.status(401).send({'error': 'You dont have permission to call this route'});
    }

    // If payload is available
    if (data) {
        // Check if battle pass level reward has been claimed
        if (payload.webhook_type === 'battlepass-level-reward-claimed') {
            if (payload.battlepass_level_reward_claimed.reward_type_name === 'Item') {
                unlockItem(payload.battlepass_level_reward_claimed.reward_amount);
            }
        }
    }

    // Return a 200 status code so that SCILL knows everything went fine
    res.send({message: 'OK'});
});

app.listen(port, () => {
    console.log('Example app listening at http://localhost:${port}');
});
No example code available for PHP
Another way of handling the Webhook result is by implementing an object compare function that will diff the two objects and will return an object just containing the differences. A very good example on how to do that is provided in this blog post: [Getting the differences between two objects with vanilla JS](https://gomakethings.com/getting-the-differences-between-two-objects-with-vanilla-js/).
Data sent to your Webhook
{
  "new_challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "unlock",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  },
  "old_challenge":   {
    "challenge_id": "505538946732425217",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 500,
    "live_date": "2020-10-12T00:00:00Z",
    "challenge_goal": 5,
    "user_challenge_current_score": 0,
    "challenge_icon": "black-arrow",
    "challenge_icon_hd": "black-arrow-hd",
    "challenge_price": 0,
    "challenge_reward": "weapon_a",
    "challenge_reward_type": "item",
    "challenge_goal_condition": 0,
    "challenge_xp": 0,
    "repeatable": false,
    "type": "unlock",
    "is_claimed": false,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_is_claimed": false,
    "user_challenge_status": 0
  }
}