Developers

Achievements & Game API

Steam-like achievements for your Indiade game in four steps:

  1. Open your game in Studio → Achievements: add name, description, icon, points and the secret flag.
  2. Copy the API name (e.g. FIRST_WIN). A game key + secret is created with the first achievement.
  3. Call unlock from your game – browser games use the JS SDK (no keys), desktop games the signed REST API.
  4. 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