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. unlocked
  2. inactive
  3. in-progress
  4. unclaimed or overtime

Every challenge is unlocked 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.

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.

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_duration_time integer

The duration of the challenge in seconds. Challenges auto lock after time-out and need to be unlocked again.

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_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 integer

Some challenges have a reward if they are achieved. You can set an integer value here which can be an identifier for your own reward system, number of coins. You are free to use that.

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
overtime
User did not manage to finish the challenge in time.
unclaimed
The challenge has been completed but the reward has not yet 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.

finished
The challenge has been successfully be completed and the reward has been claimed
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",
    "user_challenge_id": "0",
    "challenge_name": "Survive 3 battles",
    "challenge_duration_time": 1440,
    "challenge_goal": 3,
    "challenge_target": 3,
    "challenge_price": 0,
    "challenge_reward": 5,
    "challenge_xp": 50,
    "challenge_icon": "harbor_black",
    "challenge_icon_hd": "harbor_black",
    "repeatable": true,
    "live_date": "2020-04-01T00:00:00Z",
    "type": "unlock",
    "challenge_current_score": 3,
    "user_challenge_unlocked_at": null,
    "user_challenge_activated_at": null,
    "user_challenge_status": 0,
    "is_claimed": false
  }
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);

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.

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.

The Challenge Category Object
  {
    "is_daily_category": false,
    "category_position": 2,
    "category_slug": "",
    "category_name": "Beginner Challenges",
    "category_id": 543133912041586700,
    "challenges": [
      {
        "challenge_id": "505538946732425217",
        "user_challenge_id": "0",
        "challenge_name": "Survive 3 battles",
        "challenge_duration_time": 1440,
        "challenge_goal": 3,
        "challenge_target": 3,
        "challenge_price": 0,
        "challenge_reward": 5,
        "challenge_xp": 50,
        "challenge_icon": "harbor_black",
        "challenge_icon_hd": "harbor_black",
        "repeatable": true,
        "live_date": "2020-04-01T00:00:00Z",
        "type": "unlock",
        "challenge_current_score": 3,
        "user_challenge_unlocked_at": null,
        "user_challenge_activated_at": null,
        "user_challenge_status": 0,
        "is_claimed": false
      }  
    ]
  }

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.

Base URL

https://pcs.scillgame.com

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/get/: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 will be personalized for the user encoded in the access token.

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 for your product and the user encoded in the access token. This will also be separated in categories as shown below.

GET api/v1/challenges/personal/get/:id
const scill = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const categories = await scill.getPersonalChallenges('example-product', accessToken);
categories.forEach((category => {
  category.challenges.forEach((challenge => {
    console.log(challenge);
  }));
}));
The Personal Challenges Response

This endpoint returns an array of Category objects.

[
  {
    "is_daily_category": false,
    "category_position": 2,
    "category_slug": "",
    "category_name": "Beginner Challenges",
    "category_id": 543133912041586700,
    "challenges": [
      {
        "challenge_id": "505538946732425217",
        "user_challenge_id": "0",
        "challenge_name": "Survive 3 battles",
        "challenge_duration_time": 1440,
        "challenge_goal": 3,
        "challenge_target": 3,
        "challenge_price": 0,
        "challenge_reward": 5,
        "challenge_xp": 50,
        "challenge_icon": "harbor_black",
        "challenge_icon_web": "harbor_black",
        "repeatable": true,
        "live_date": "2020-04-01T00:00:00Z",
        "type": "unlock",
        "challenge_current_score": 3,
        "user_challenge_purchased_at": null,
        "user_challenge_activated_at": null,
        "user_challenge_status": 0,
        "is_claimed": false
      }
    ]
  }
]

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.

GET api/v1/challenges/personal/unlock/:pid/:cid
const scill = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const userChallengeId = await scill.unlockChallenge('example-product', 'example-challenge', accessToken);
The Buy Challenge Response
{
  "message":"OK",
  "status":200
}

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.

GET api/v1/challenges/personal/activate/:pid/:cid
const scill = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const userChallengeId = await scill.unlockChallenge('example-product', 'example-challenge', accessToken);
The Activate Challenge Response
{
  "message":"OK",
  "status":200
}

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.

DELETE api/v1/challenges/personal/cancel/:pid/:cid
const scill = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const userChallengeId = await scill.unlockChallenge('example-product', 'example-challenge', accessToken);
const cancelled = await scill.cancelChallenge('example-product', userChallengeId, accessToken);
The Cancel Challenge Response
{
  "message":"OK",
  "status":200
}

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.

GET api/v1/challenges/personal/claim/:pid/:cid
const scill = require('@scillgame/scill-js');
const accessToken = getAccessToken();
const userChallengeId = await scill.unlockChallenge('example-product', 'example-challenge', accessToken);
// In the meantime the challenge updated
const claimed = await scill.claimChallenge('example-product', userChallengeId);
The Cancel Challenge Response
{
  "message":"OK",
  "status":200
}

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. Using our SDK, the example above is just a few lines of code:

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
});
Subscribe to MQTT server
// Please note, in this example we use the MQTT.js JavaScript library
const scill = require('@scillgame/scill-js');
// Implemented earlier - get an access token for the current user
const accessToken = getAccessToken();
const authApi = getAuthApi(accessToken);
authApi.getUserChallengesNotificationTopic().then(notificationTopic => {
  // In browser environements we need to use the Websocket protocol
  const client = mqtt.connect('ws://mqtt.scillgame.com:8083/mqtt');
  client.on('connect', () => {
    client.subscribe(notificationTopic.topic, function (err) {
      if (err) {
        console.warn("Subscribing to MQTT failed");
      }
    });
  });
  client.on('message', (topic, message) => {
    if (message && message.length > 0) {
      try {
        var payload = JSON.parse(message.toString("utf8"));
        if (payload) {
          // You now got a payload of type ChallengeWebhookPayload. Update UI, or implement business logic.
        }
      } catch (e) {
        console.warn("MQTT payload could not be parsed", topic, message.toString("utf8"));
      }
    }
  });
});

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:

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.

Processing data from Webhook
webSocket.onmessage = function (event) {
  console.log('Received Webhook', event.data);
  const data = JSON.parse(event.data);
  if (data) {        
    if (data.new_challenge.user_challenge_current_score !== data.old_challenge.user_challenge_current_score) {
      // The challenge made progress
    } else if (data.new_challenge.type !== data.old_challenge.type) {
      if (data.new_challenge.type === 'finished' && data.old_challenge.type !== 'finished') {
        // This challenge got claimed, give reward to user
      } else {
        // Challenge action (got unlocked, activated, claimed, etc)      
      }      
    }
  }
}

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.

Data sent to Webhook
{
  "new_challenge": {
    "app_id": "597737952688570400",
    "challenge_goal": 200,
    "challenge_id": "597804844222513200",
    "challenge_price": 0,
    "challenge_reward": 0,
    "challenge_type": "kill-enemy",
    "challenge_xp": 0,
    "session_id": "1234",
    "time_achieved": 0,
    "time_target": 0,
    "type": "in-progress",
    "user_challenge_activated_at": "2020-10-13T10:34:52.194122Z",
    "user_challenge_current_score": 7,
    "user_challenge_expires_at": "2020-10-13T13:04:52.194122Z",
    "user_challenge_is_claimed": false,
    "user_challenge_is_expired": false,
    "user_challenge_status": 0,
    "user_challenge_unlocked_at": "2020-10-13T10:34:49.775132Z",
    "user_id": "1234",    
  },
  "old_challenge": {
    "app_id": "597737952688570400",
    "challenge_goal": 200,
    "challenge_id": "597804844222513200",
    "challenge_price": 0,
    "challenge_reward": 0,
    "challenge_type": "kill-enemy",
    "challenge_xp": 0,
    "session_id": "1234",
    "time_achieved": 0,
    "time_target": 0,
    "type": "in-progress",
    "user_challenge_activated_at": "2020-10-13T10:34:52.194122Z",
    "user_challenge_current_score": 6,
    "user_challenge_expires_at": "2020-10-13T13:04:52.194122Z",
    "user_challenge_is_claimed": false,
    "user_challenge_is_expired": false,
    "user_challenge_status": 0,
    "user_challenge_unlocked_at": "2020-10-13T10:34:49.775132Z",
    "user_id": "1234",    
  }
}