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 compliance is 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.
Challenges have a lifetime cycle that is reflected in the type
attribute of the Challenge
object.
unlock
unlocked
in-progress
unclaimed
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.
Challenges have a lifetime cycle that is reflected in the type
attribute of the Challenge
object.
unlock
unlocked
in-progress
[lost]
unclaimed
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 current 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.
challenge_auto_activated boolean
Indicates if the challenges' lifecycle is handled automatically by the SCILL backend. Use this flag to decide when to show action buttons for unlocking, activating, claiming or canceling challenges. Hide the buttons if this flag is true, and let the user manage challenges manually if this flag is false.
can_be_auto_activated boolean
Indicates if the challenge can be auto-activated. Applies only to periodic challenges.
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!
{
"challenge_id": "505538946732425217",
"challenge_name": "Survive 3 battles",
"challenge_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"is_claimed": false,
"user_challenge_unlocked_at": null,
"user_challenge_activated_at": null,
"user_challenge_is_claimed": false,
"user_challenge_status": 0
}
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);
}
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.
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.
{
"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_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"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.
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.
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.
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.
// 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
}
}
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);
}));
}));
});
[
{
"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_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"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.
// 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);
}
}
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
}
}));
{
"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.
// 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);
}
}
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
}
}));
{
"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: 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.
// 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);
}
}
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
}
}));
{
"status": 200,
"message": "OK",
"challenge": {
"challenge_id": "505538946732425217",
"challenge_name": "Survive 3 battles",
"challenge_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"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.
// 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);
}
}
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
}
}));
{
"status": 200,
"message": "OK",
"challenge": {
"challenge_id": "505538946732425217",
"challenge_name": "Survive 3 battles",
"challenge_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"is_claimed": false,
"user_challenge_unlocked_at": null,
"user_challenge_activated_at": null,
"user_challenge_is_claimed": false,
"user_challenge_status": 0
}
}
Reset user challenges
Resets all user challenges for the given app ID and user ID
URL | /api/v1/reset-challenges/:pid/:uid |
Method | DELETE |
Authentication | Your application API key |
Parameters
pid integer REQUIRED
The unique id of the product. The product ID is listed in the Admin Panel. This is your application ID.
uid integer REQUIRED
The unique user id. This is the string field you send us to identify your user.
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.
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 |
{
"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.
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.
// 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
});
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
});
// 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;
}
}
// 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"));
}
}
});
});
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: 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 asynchronously 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.
// 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}');
});
{
"webhook_type": "challenge-changed",
"category_position": 1,
"user_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ...",
"new_challenge": {
"challenge_id": "505538946732425217",
"challenge_name": "Survive 3 battles",
"challenge_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"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_description": "Get a great reward by managing this challenge. You can try as long as you like",
"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",
"challenge_auto_activated": true,
"is_claimed": false,
"user_challenge_unlocked_at": null,
"user_challenge_activated_at": null,
"user_challenge_is_claimed": false,
"user_challenge_status": 0
}
}