Achievements & Game API
Steam-like achievements for your Indiade game in four steps:
- Open your game in Studio → Achievements: add name, description, icon, points and the secret flag.
- Copy the API name (e.g. FIRST_WIN). A game key + secret is created with the first achievement.
- Call unlock from your game – browser games use the JS SDK (no keys), desktop games the signed REST API.
- Players see an unlock toast in the Indiade app, and the achievement appears in their app profile and on your game’s page in the app (with global unlock %).
Browser games (HTML5) – JS SDK
Achievements are an Indiade app feature: when a player opens your browser game in the app, it runs in a sandboxed iframe in the app’s player and the SDK talks to the app via postMessage – no keys in your code. On indiade.com the calls are harmless no-ops (init() reports offline), so you can ship one build everywhere.
<script src="https://indiade.com/sdk/indiade.js"></script>
<script>
const me = await Indiade.init(); // { loggedIn, username }
// … when the player wins:
const r = await Indiade.unlock("FIRST_WIN"); // { ok, newlyUnlocked }
const list = await Indiade.achievements(); // name, unlockedAt, globalPct…
</script>Desktop games – REST API with HMAC
When a player starts your game from the Indiade app, it passes a player token (valid 24 h, only for your game). Sign each request with your game secret.
# The Indiade app starts your game with:
# env INDIADE_PLAYER_TOKEN=idp_… INDIADE_API=https://indiade.com/api/game/v1 INDIADE_GAME_KEY=gk_…
# args --indiade-token=idp_…
# Every request is signed: X-Indiade-Key: <game key>
# X-Indiade-Timestamp: <unix seconds>
# X-Indiade-Signature: hex(HMAC_SHA256(secret, ts + "\n" + METHOD + "\n" + path?query + "\n" + body))
POST /api/game/v1/achievements/unlock
Content-Type: application/json
{"player_token":"idp_…","achievement":"FIRST_WIN"}
→ 200 {"newlyUnlocked":true,"achievement":{"apiName":"FIRST_WIN","name":"First victory","points":10,…}}
GET /api/game/v1/achievements?player_token=idp_…
→ 200 {"player":{"username":"…"},"achievements":[{"apiName":"FIRST_WIN","unlocked":true,"globalPct":12.5,…}]}
# shell example
TS=$(date +%s); BODY='{"player_token":"'$INDIADE_PLAYER_TOKEN'","achievement":"FIRST_WIN"}'
SIG=$(printf '%s\nPOST\n/api/game/v1/achievements/unlock\n%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/.* //')
curl -X POST https://indiade.com/api/game/v1/achievements/unlock -H "Content-Type: application/json" \
-H "X-Indiade-Key: $INDIADE_GAME_KEY" -H "X-Indiade-Timestamp: $TS" -H "X-Indiade-Signature: $SIG" -d "$BODY"Unity (C#)
// Unity (C#) – IndiadeAchievements.cs
using System; using System.Text; using System.Collections; using System.Security.Cryptography;
using UnityEngine; using UnityEngine.Networking;
public class IndiadeAchievements : MonoBehaviour {
const string Api = "https://indiade.com/api/game/v1";
public string gameKey = "gk_…"; // from the Indiade game editor
public string secret = "gs_…"; // prefer your own server for this in competitive games
string Token => Environment.GetEnvironmentVariable("INDIADE_PLAYER_TOKEN") ?? Arg("--indiade-token=");
static string Arg(string p) { foreach (var a in Environment.GetCommandLineArgs()) if (a.StartsWith(p)) return a.Substring(p.Length); return null; }
public void Unlock(string id) { if (Token != null) StartCoroutine(Send(id)); }
IEnumerator Send(string id) {
var path = "/api/game/v1/achievements/unlock";
var body = "{\"player_token\":\"" + Token + "\",\"achievement\":\"" + id + "\"}";
var ts = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var sig = BitConverter.ToString(h.ComputeHash(Encoding.UTF8.GetBytes(ts + "\nPOST\n" + path + "\n" + body))).Replace("-", "").ToLower();
var req = new UnityWebRequest("https://indiade.com" + path, "POST");
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Content-Type", "application/json");
req.SetRequestHeader("X-Indiade-Key", gameKey);
req.SetRequestHeader("X-Indiade-Timestamp", ts);
req.SetRequestHeader("X-Indiade-Signature", sig);
yield return req.SendWebRequest();
Debug.Log("Indiade unlock " + id + ": " + req.responseCode + " " + req.downloadHandler.text);
}
}Godot 4 (GDScript)
# Godot 4 (GDScript) – autoload "Indiade"
extends Node
const API := "https://indiade.com"
@export var game_key := "gk_…"
@export var secret := "gs_…"
func _token() -> String:
var t := OS.get_environment("INDIADE_PLAYER_TOKEN")
if t == "":
for a in OS.get_cmdline_args():
if a.begins_with("--indiade-token="): t = a.trim_prefix("--indiade-token=")
return t
func unlock(id: String) -> void:
var token := _token()
if token == "": return
var path := "/api/game/v1/achievements/unlock"
var body := JSON.stringify({"player_token": token, "achievement": id})
var ts := str(int(Time.get_unix_time_from_system()))
var ctx := HMACContext.new()
ctx.start(HashingContext.HASH_SHA256, secret.to_utf8_buffer())
ctx.update(("%s\nPOST\n%s\n%s" % [ts, path, body]).to_utf8_buffer())
var sig := ctx.finish().hex_encode()
var http := HTTPRequest.new(); add_child(http)
http.request_completed.connect(func(_r, code, _h, b): print("Indiade unlock ", id, ": ", code, " ", b.get_string_from_utf8()); http.queue_free())
http.request(API + path, ["Content-Type: application/json", "X-Indiade-Key: " + game_key, "X-Indiade-Timestamp: " + ts, "X-Indiade-Signature: " + sig], HTTPClient.METHOD_POST, body)Security notes
- Anything shipped inside a game client can be extracted. For competitive/valuable achievements, call the API from your own server and keep the secret there.
- Player tokens are scoped to one game and one player and expire after 24 h. Unlocks are idempotent.
- Timestamps must be within ±5 minutes. Rotate the key in the editor if it leaks.