Two services, shared by our apps
A ranked-board service and a key-value store. Both are generic: they hold what an app gives them and never interpret it.
Authentication
Every call carries an app key in X-Api-Key. Calls made on a player's
behalf also carry that player's token as Authorization: Bearer ….
-
Publishable key (
pk_…) ships inside a client binary. It is not a secret and is not treated as one: it can register a player, and act for the player whose token accompanies it. -
Secret key (
sk_…) is server-side only. It adds the operations that would be abuse in a client's hands — writing app-scoped values, and acting for a named player.
A key is shown once, when it is created; only its hash is stored. The same is true of a player token. Neither can be recovered — they are replaced.
Registering a player
curl -X POST https://www.outworldapps.com/api/v1/players \
-H "X-Api-Key: pk_…" \
-H "Content-Type: application/json" \
-d '{"displayName":"Ada","deviceLabel":"Pixel 8"}'
The response carries the player and a token. Send externalId as well if the
app has its own stable id for the player and wants a second device to join the same
identity — but only if that id is unguessable, because it is not authenticated.
Leaderboards
A board's rules — direction, aggregation, period, rollover zone — belong to the board and are set in the back office. A submission carries a score and an opaque blob, and cannot influence how it is ranked. The instant a score was achieved is the server's clock, never the caller's, because it is also the tie-break.
Server-verified boards. A board can be set to refuse anything but a
secret key. Use it when your own server decides whether a result is real — a replay it
re-simulates, a purchase it checks with a store, a match it refereed. On an ordinary
board the publishable key is in the client binary and the player token is on the
device, so whatever score a client claims is what the board records; that is fine for a
casual high-score table and worthless for anything you intend to mean something. Submit
with the secret key and ?playerId= to name the player the score belongs to.
Reads are unaffected — a client still renders the board with its publishable key.
period, offset, limit?playerId= with a secret keycurl -X POST https://www.outworldapps.com/api/v1/leaderboards/all-time/scores \
-H "X-Api-Key: pk_…" \
-H "Authorization: Bearer pt_…" \
-H "Content-Type: application/json" \
-d '{"score":18452,"metadata":{"highestTile":2048,"moves":712}}'
The response says whether the standing actually changed
(improved), what it was before, the new rank, and where that rank sits in
the board as a percentage. improved: false is a success — it is what a
player who did not beat their own best gets.
Aggregation
- Best — keep the player's best submission for the period. A high-score table.
- Latest — keep the most recent, better or worse. A rating that can fall.
- Cumulative — add submissions up. Totals: steps, coins, kills this week.
Periods
AllTime uses the single key all. Daily is
2026-08-10, Weekly is the ISO week 2026-W33, and
Monthly is 2026-08. Boundaries fall in the board's rollover
time zone, not the caller's and not necessarily UTC. Pass ?period= to read a
past board.
Limits: page size up to 100 (default 25); metadata up to 4 KB of valid JSON.
Key-value store
Namespaced storage for what is not a score: cloud saves, settings, an app-wide configuration blob. Values are opaque and optionally expire.
?scope=player (the default) is the calling player's own space.
?scope=app is shared by every installation: readable with any key, writable
only with a secret one. A secret key may also pass ?playerId= to act for a
named player.
Conflicts are the point
Two devices syncing one save is the normal case. Every value carries a
revision, returned as an ETag; send it back as
If-Match and a write that would clobber somebody else's is refused with
409 — and the refusal carries the current value, so the client can merge
rather than re-fetch. If-Match: 0 means "only if nothing is there".
curl -X PUT https://www.outworldapps.com/api/v1/kv/saves/slot-1 \
-H "X-Api-Key: pk_…" \
-H "Authorization: Bearer pt_…" \
-H "If-Match: 4" \
-H "Content-Type: application/json" \
-d '{"value":{"seed":91117,"actions":"uldr…"}}'
Limits: 256 KB per value;
200 keys per player;
5000 app-scoped keys. Namespaces and keys are letters,
digits and - _ . :; keys may also contain /.
Errors and limits
Every failure is the same shape: a stable code to branch on and a
message for whoever is reading the log. A 429 always carries
Retry-After — use it rather than guessing, because a guessing client is how
a rate limit becomes a retry storm.
{ "code": "revisionMismatch", "message": "The stored revision does not match …" }
Rate limits are per player where one is known and per app-and-address otherwise: 300 reads and 60 writes per 60 seconds.