Add an Online Leaderboard to Your Game in 15 Minutes
Copy-paste tutorial: create a leaderboard, then submit and fetch scores from Unity (C#) and Godot (GDScript) against a live API. No servers to run.
A leaderboard is a small, concrete way to add an online loop to a game. You do not need to run a server or stand up a ranking database yourself. This is a copy-paste tutorial for wiring a working leaderboard into a Unity or Godot project using the live Crux API; the endpoints below are the actual contract.
We make Crux, so yes, this funnels toward our free tier. But the code is real and the pattern is the same for any HTTP backend, so you learn something portable either way.
Step 1 - create a free account and grab your keys (3 minutes)
- Sign up for Crux - it is free (10,000 monthly active users, 2 million API calls a month, no credit card). A project and a
devenvironment are created for you automatically. - In the dashboard, copy your project ID and environment ID (both are UUIDs), then copy the publishable key for player code. It is safe to ship; keep the separate secret key on a trusted server.
- Right there in the dashboard, hit "Run your first call" - a one-click button that fires a real authenticated request so you can see a
200 OKbefore you write any code.
Optional: prove the key from your terminal
If you would rather see it work from a shell first, this single call mints a guest player and proves your key is live:
# Prove your API key works - mint a guest player
curl -X POST https://crux.supercraft.host/v1/auth/anonymous \
-H "Authorization: ApiKey <YOUR_PUBLISHABLE_KEY>" \
-H "Content-Type: application/json" \
-d '{"anonymous_id":"device-abc-123","display_name":"Ada"}'
# 200 OK -> { "player_id": "7f3c1e2a-...", "access_token": "eyJhbGci...", ... }
Step 2 - create a leaderboard (2 minutes)
In the dashboard, create a leaderboard: give it a key like high_scores and a sort order of desc (highest score first). You can address it by that key from here on - or copy its ID (a UUID) if you prefer. That is the last value you need. You now have four things: base URL, project ID, environment ID, and leaderboard ID, plus your API key.
The whole contract, in curl
Before the engine code, here is the entire leaderboard API - two calls. Everything below is just these wrapped in Unity or Godot:
CRUX=https://crux.supercraft.host
PROJECT=<PROJECT_ID> # UUID from the dashboard
ENV=<ENV_ID> # UUID from the dashboard
KEY=<YOUR_PUBLISHABLE_KEY> # safe to ship in a player client
BOARD=high_scores # your leaderboard's key (its UUID works too)
# Submit a score
curl -X POST "$CRUX/v1/projects/$PROJECT/environments/$ENV/leaderboards/$BOARD/entries/" \
-H "Authorization: ApiKey $KEY" -H "Content-Type: application/json" \
-d '{"player_id":"ada","score":12750}'
# Read the top 10
curl "$CRUX/v1/projects/$PROJECT/environments/$ENV/leaderboards/$BOARD/entries/top?limit=10" \
-H "Authorization: ApiKey $KEY"
# [ {"player_id":"ada","score":12750,"rank":1}, ... ]
Notice the player_id is just a string you pick - here it's a name. No login required to appear on the board. (More on when you do want real identity at the end.)
Step 3a - submit and fetch from Unity (C#)
Drop this MonoBehaviour into your project. It uses Unity's built-in UnityWebRequest - no package to install - and hits the exact two endpoints from above.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Text;
public class Leaderboard : MonoBehaviour
{
const string CRUX = "https://crux.supercraft.host";
const string PROJECT = "<PROJECT_ID>"; // a UUID from the dashboard
const string ENV = "<ENV_ID>"; // a UUID from the dashboard
const string API_KEY = "<YOUR_PUBLISHABLE_KEY>"; // safe to ship in a player client
const string BOARD = "high_scores"; // your leaderboard's key (UUID works too)
string EntriesUrl(string suffix) =>
CRUX + "/v1/projects/" + PROJECT + "/environments/" + ENV +
"/leaderboards/" + BOARD + "/entries" + suffix;
[System.Serializable]
class ScoreSubmit { public string player_id; public float score; }
// Submit a score: POST .../entries/
public IEnumerator SubmitScore(string playerId, float score)
{
string json = JsonUtility.ToJson(new ScoreSubmit { player_id = playerId, score = score });
using var req = new UnityWebRequest(EntriesUrl("/"), "POST");
req.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(json));
req.downloadHandler = new DownloadHandlerBuffer();
req.SetRequestHeader("Authorization", "ApiKey " + API_KEY);
req.SetRequestHeader("Content-Type", "application/json");
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
Debug.LogError("submit failed: " + req.responseCode + " " + req.error);
}
// Fetch the top N: GET .../entries/top?limit=N
public IEnumerator GetTop(int limit = 10)
{
using var req = UnityWebRequest.Get(EntriesUrl("/top?limit=" + limit));
req.SetRequestHeader("Authorization", "ApiKey " + API_KEY);
yield return req.SendWebRequest();
if (req.result != UnityWebRequest.Result.Success)
{
Debug.LogError("get top failed: " + req.responseCode + " " + req.error);
yield break;
}
// Response is a JSON array: [ {"player_id","score","rank"}, ... ]
Debug.Log(req.downloadHandler.text);
// Parse with Newtonsoft (handles top-level arrays directly), or wrap
// the string for Unity's JsonUtility. Each row has player_id, score, rank.
}
}
// Call them from anywhere:
// StartCoroutine(leaderboard.SubmitScore("ada", 12750));
// StartCoroutine(leaderboard.GetTop(10));
Prefer a typed SDK over raw HTTP? The same two calls are SubmitScoreAsync and GetTopAsync in the official Unity package - see the SDK quickstart.
Step 3b - submit and fetch from Godot (GDScript / HTTPRequest)
Add this as an autoload named Leaderboard (Project → Project Settings → Autoload). It uses Godot's built-in HTTPRequest - no addon required - and hits the identical endpoints.
extends Node
## A tiny leaderboard client for Crux using Godot's HTTPRequest.
const BASE := "https://crux.supercraft.host"
const PROJECT := "<PROJECT_ID>" # a UUID from the dashboard
const ENV := "<ENV_ID>" # a UUID from the dashboard
const API_KEY := "<YOUR_PUBLISHABLE_KEY>" # safe to ship in a player client
const BOARD := "high_scores" # your leaderboard's key (UUID works too)
signal scores_received(scores) # Array of { player_id, score, rank }
func _entries(suffix: String) -> String:
return "%s/v1/projects/%s/environments/%s/leaderboards/%s/entries%s" % [BASE, PROJECT, ENV, BOARD, suffix]
func _headers() -> PackedStringArray:
return PackedStringArray([
"Authorization: ApiKey " + API_KEY,
"Content-Type: application/json",
])
# Submit a score: POST .../entries/
func submit_score(player_name: String, score: float) -> void:
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(_result, code, _headers, _body):
http.queue_free()
if code >= 400:
push_error("submit failed: HTTP %d" % code)
)
var payload := JSON.stringify({ "player_id": player_name, "score": score })
http.request(_entries("/"), _headers(), HTTPClient.METHOD_POST, payload)
# Fetch the top N: GET .../entries/top?limit=N
func get_top(limit: int = 10) -> void:
var http := HTTPRequest.new()
add_child(http)
http.request_completed.connect(func(_result, code, _headers, body):
http.queue_free()
if code >= 400:
emit_signal("scores_received", [])
return
var scores = JSON.parse_string(body.get_string_from_utf8())
emit_signal("scores_received", scores if scores is Array else [])
)
http.request(_entries("/top?limit=%d" % limit), _headers(), HTTPClient.METHOD_GET)
Then submit a score and print the board:
func _ready() -> void:
Leaderboard.scores_received.connect(_on_scores)
Leaderboard.submit_score("ada", 12750)
Leaderboard.get_top(10)
func _on_scores(scores: Array) -> void:
for row in scores:
print("#%d %s %d" % [row.rank, row.player_id, int(row.score)])
That's it - you have an online leaderboard
Fifteen minutes, two endpoints, no servers to run. If you are coming off a shut-down Godot backend, this is the same shape as the SilentWolf migration - a name, a score, and the top N.
When you outgrow "just a string"
Two upgrades you will likely want later:
- Real player identity. Swap the arbitrary
player_idstring for a real player: callPOST /v1/auth/anonymous(guest) or register with email/OAuth, and use the returnedplayer_id. Then a score follows the same human across devices. The SDK quickstart shows the auth call. - Anti-cheat. A client-embedded API key means a determined player can extract it and post fake scores - fine for casual boards, not for competitive ones. For those, submit from a trusted server using a server token and validate the run server-side. Pattern: stopping leaderboard cheating with server-side validation.
Start free
Everything above runs on the free Dev tier - 10,000 MAU, every feature, no credit card. The fastest path is the 2-minute SDK quickstart, and remember the dashboard's one-click "Run your first call" button proves your key before you write a line of engine code. And because backends have a habit of changing the deal (see below), it is worth knowing your data is portable from the first call - the export guarantee spells that out.
Start free - the 2-minute quickstart →
Related reading
- SilentWolf shut down: migrate your Godot leaderboards in 15 minutes
- The best Godot backend in 2026
- PlayFab alternatives for indie games in 2026
- Will your game backend still be alive next year?
- The Crux sunset & export guarantee
Shipping to the browser instead? WebGL builds have their own constraints - no files, no raw sockets, no native plugins - and they change how you wire this up.