Logo of RoyalUr.net
HomeRulesAboutBlog
Sign In
The Game
HomePlayRulesRegister
Policies
Privacy PolicyTerms & ConditionsCookie Policy
About Us
AboutBlogKo-FiContact Us
RoyalUr.netby Padraig Lamont
© Rosette Games Ltd, 20252.3.0
FacebookInstagramDiscordRedditTwitter
Home

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/me

Generate 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_typeHTTPMeaning
invalid_api_key401Missing, malformed, or revoked key
not_found404Unknown user, game, or route
bad_request400Invalid parameter (the message says which)
rate_limited429Over 600 requests/minute. The Retry-After header says how many seconds to wait
internal_server_error500Our fault!