Developer API
Programmatic access to public RoyalUr.net data: games, players, ratings, and leaderboards.
Base URL: https://api.royalur.net/api/v1
This API is provided best-effort and may change without notice.
Authentication
Every route requires a personal API key, passed as a bearer token:
curl -H "Authorization: Bearer YOUR_API_KEY" \
https://api.royalur.net/api/v1/meGenerate your key at royalur.net/settings/developer. One key per account; regenerating burns the old key. Requests are rate-limited to 600 per minute per key, over a fixed one-minute window (HTTP 429 when exceeded). There is no separate per-second limit, so a burst is fine as long as the minute holds.
Use the API from your own server, not from a browser. We do not send CORS headers for other origins, so browser requests will be blocked.
Routes
GET/me
Identifies the account that owns your API key. Handy for checking that a key works.
{"user": {"public_id": "...", "username": "paddy", "display_name": "Paddy",
"created_at": "2024-02-24T12:16:40Z"},
"key": {"prefix": "rgu_6w5h", "created_at": "2026-07-24T12:45:15Z"}}GET/users/by_username/{username}
Look up a player by username, returning their profile and current rating on each ranked season. Use this to resolve a username to the public_id that other routes take. Ratings are not Elo: each player's rating converges towards a target set by how accurately they play, so rating changes are not zero-sum between opponents.
{"user": {"public_id": "...", "username": "...", "display_name": "...",
"created_at": "..."},
"ratings": [{"season": "ranked_finkel", "rating": 72.3}]}GET/users/{userPublicID}
The same profile + ratings response, looked up by public ID.
GET/users/{userPublicID}/rating_history
Daily rating snapshots for one player on one season. season is required: one of ranked_finkel, ranked_blitz, or ranked_masters.
GET /users/{userPublicID}/rating_history?season=ranked_finkel
{"user_public_id": "...", "season": "ranked_finkel",
"history": [{"date": 20250113, "final_rating": 72.3, "max_rating": 74.7,
"min_rating": 72.3, "avg_rating": 73.5}]}GET/games
Lists finished games, newest first. Query parameters, all optional:
- player: a user public ID. Only games that player was in.
- settings: a ruleset id of finkel, blitz, masters3d, or custom. Older games may also carry masters, the retired 4-dice version of the Masters ruleset. New games never use it, but it is still returned in historical results, so handle it.
- mode: a game mode of local, computer, online, friend, or ranked.
- season: a ranked season of ranked_finkel, ranked_blitz, or ranked_masters. Only ranked games from that season.
- before_game: a game public ID. Only games older than it. This is how you page: pass the public_id of the last game in a response to get the next page.
- before: an ISO-8601 timestamp, such as 2026-01-01T00:00:00Z. Only games that ended before it. A time filter, not a cursor: use before_game to page.
- limit: max games per response (default 50, max 200).
Each game includes its players and, when the game has been analysed, an analysis block: move counts, mean roll deltas (luck), mean move deltas (accuracy), and rating changes for ranked games. A player who has since deleted their account appears as a null user.
{"games": [{"public_id": "...", "mode": "ranked", "settings": "finkel",
"season": "ranked_finkel", "end_reason": "win",
"did_light_win": true, "started_at": "...", "ended_at": "...",
"light_player": {"bot": null, "user": {"public_id": "...",
"username": "...", "display_name": "...",
"created_at": "..."}},
"dark_player": {"bot": "panda", "user": null},
"analysis": {"light_move_count": 67, "dark_move_count": 58,
"light_mean_roll_delta": -0.21, "dark_mean_roll_delta": 0.4,
"light_mean_move_delta": 1.8, "dark_mean_move_delta": 2.5,
"light_initial_rating": 74.6, "dark_initial_rating": 19.0,
"light_rating_change": -2.3, "dark_rating_change": 2.3,
"rating_cleared": false}}],
"count": 1}GET/games/{gamePublicID}
One game in full, including the complete move-by-move record (states, rolls, moves) in RoyalUr.net's JSON game notation. The game carries every field listings give you, plus a contents block that only this route returns. The metadata block is optional, so read a game's times from started_at and ended_at.
initial_state is the opening position, and states holds every state after it in order. Only initial_state carries a board, so you rebuild each later position by applying the moves.
Tiles are signed path positions. Positive numbers are light, negative are dark, and both count from 1 along that player's own path. In a move, src is absent when the move introduces a piece, dest is absent when it scores one, and captured appears only on a capture. A roll state is a roll that left the player no legal move. Each time is milliseconds since the game started. The signs in dies say which dice landed up. The magnitudes are random numbers used to pick the dice variants to render, and are not important to gameplay.
{"game": {"public_id": "...", "mode": "ranked", "settings": "finkel",
"season": "ranked_finkel", "end_reason": "win",
"did_light_win": true, "started_at": "...", "ended_at": "...",
"light_player": {...}, "dark_player": {...}, "analysis": {...},
"contents": {
"notation_version": 2,
"metadata": {"StartTime": "...", "EndTime": "...",
"TimeControl": "21 seconds per move"},
"settings": {"board_shape": "standard", "paths": "bell",
"dice": "four_binary", "start_pieces": 7,
"safe_rosettes": true, "rosettes_grant_rolls": true,
"captures_grant_rolls": false},
"initial_state": {"type": "wait4roll", "time": 0, "turn": "L",
"board": {"pieces": {}},
"players": {"L": {"pieces": 7, "score": 0},
"D": {"pieces": 7, "score": 0}}},
"states": [{"type": "move", "time": 5888, "turn": "L",
"roll": {"value": 2, "dies": [0.21, 0.33, -0.2, -0.27]},
"move": {"dest": 2}},
{"type": "roll", "time": 22276, "turn": "L",
"roll": {"value": 0, "dies": [-0.07, -0.1, -0.08, -0.48]},
"moves": []},
{"type": "move", "time": 129013, "turn": "D",
"roll": {"value": 3, "dies": [0.15, 0.99, 0.09, -0.11]},
"move": {"src": -4, "dest": -7, "captured": 7}},
{"type": "win", "time": 825107, "winner": "L"}]}}}GET/leaderboard
The top-rated players on a season. season is required: one of ranked_finkel, ranked_blitz, or ranked_masters. count defaults to 100 (max 1000).
GET /leaderboard?season=ranked_finkel&count=100
{"season": "ranked_finkel",
"entries": [{"rank": 1, "rating": 72.3, "user": {"public_id": "...",
"username": "...", "display_name": "...",
"created_at": "..."}}]}Errors
Errors are JSON with an error_type and human-readable message:
{"error_type": "invalid_api_key", "message": "...", "data": {}}| error_type | HTTP | Meaning |
|---|---|---|
| invalid_api_key | 401 | Missing, malformed, or revoked key |
| not_found | 404 | Unknown user, game, or route |
| bad_request | 400 | Invalid parameter (the message says which) |
| rate_limited | 429 | Over 600 requests/minute. The Retry-After header says how many seconds to wait |
| internal_server_error | 500 | Our fault! |