A modern, production-ready Ruby client for the Riot Games API: League of Legends, TFT, VALORANT, Legends of Runeterra, Riftbound, and the Riot Account API.
Full routing-value handling, header-driven adaptive rate limiting, frozen
Data models, and Faraday transport with faraday-retry.
gem "rito"require "rito"
Rito.configure do |c|
c.api_key = ENV.fetch("RIOT_API_KEY")
c.region = :na1
end
client = Rito::Client.new
account = client.account.by_riot_id("Hide on bush", "KR1", region: :asia)
summoner = client.summoner.by_puuid(account.puuid, region: :kr)
match_ids = client.matches.ids_by_puuid(account.puuid, region: :asia, count: 5)Class-level config with per-client overrides (later wins):
Rito.api_key # default: ENV["RIOT_API_KEY"]
Rito.bearer_token # default: ENV["RIOT_RSO_TOKEN"] (RSO endpoints)
Rito.region # default routing value, e.g. :na1
Rito.max_retries # 429 / transport retries (default: 3)
Rito.rate_limiter # rate limit strategy (default: AdaptiveLimiter)
# per-client, for multi-key apps:
client = Rito::Client.new(api_key: "RGAPI-...", region: :kr)
rso_client = Rito::Client.new(bearer_token: access_token, region: :americas)Explicit credentials select API-key or bearer authentication without inheriting
the other credential from global config. Use separate clients for API-key and
RSO requests; configuring both credentials on one client raises ConfigError.
Explicit nil clears a credential.
Rito::Queues is a frozen, built-in reference for League queue ids as
they appear in match data (match-v5 queueId, the queue filter on
match lists). Snapshot of the League client's game select data (408 ids).
Rito::Queues.find(420).name # => "Ranked Solo/Duo"
Rito::Queues[450].mode # => :aram
Rito::Queues.find(2450).limited_time # => true (rotating game mode)
Rito::Queues.select { |q| q.category == :bots }.map(&:name).first(3)
match = client.matches.by_id("NA1_123")
match.info.queue.name # => "Ranked Flex" (convenience on MatchInfo)Each queue is a frozen Data object: id, name, short_name,
description, detailed_description, category (:pvp, :bots,
:custom), mode (:summoners_rift, :aram, :tft, :jade,
:other), limited_time, bot_honoring_allowed. The module includes
Enumerable. Unknown ids return nil (Riot adds queues without notice).
Rito::RSO implements the OAuth2 authorization code flow against
https://auth.riotgames.com. Register a client with Riot first; the
redirect URI must be on its allowlist.
rso = Rito::RSO::Client.new(
client_id: ENV.fetch("RIOT_RSO_CLIENT_ID"),
client_secret: ENV.fetch("RIOT_RSO_CLIENT_SECRET"), # or client_assertion: / private_key:
redirect_uri: "https://app.example.com/oauth2-callback"
)
# 1. Send the player to Riot; persist request.state to compare on callback
login = rso.authorization_url(login_hint: "na1|daguava")
login.url # => "https://auth.riotgames.com/authorize?..."
login.state
# 2. Exchange the ?code= query param from the callback for tokens
tokens = rso.exchange_code(params[:code])
tokens.access_token # Bearer token for RSO resources (encrypted, opaque)
tokens.id_token # signed JWT identity token
tokens.refresh_token # signed JWT, self-contained
tokens.expires_in # 600
# 3. Identify the player
me = rso.userinfo(tokens.access_token)
me.sub # player sub claim
me.cpid # "NA1" when the cpid scope was requested
# 4. Rotate when the access token expires
tokens = rso.refresh(tokens.refresh_token)Newer RSO clients authenticate with private-key JWT instead of a secret (the gem mints a fresh signed assertion per token request):
rso = Rito::RSO::Client.new(
client_id: ENV.fetch("RIOT_RSO_CLIENT_ID"),
private_key: OpenSSL::PKey::RSA.new(ENV.fetch("RIOT_RSO_PRIVATE_KEY")),
redirect_uri: "https://app.example.com/oauth2-callback"
)or pass the pre-signed client assertion (the "100 year token") via
client_assertion: and treat it like a password.
claims = rso.verify_id_token(tokens.id_token)
claims["sub"] # => signature (RS256 via /jwks.json) + iss/aud/exp validatedverify_id_token caches the JWKS document and refetches it once when a
kid is unknown (Riot rotates keypairs without disabling old ones).
Any failure raises Rito::RSO::InvalidToken. Token and userinfo failures
raise Rito::RSO::OAuthError with .error_code / .error_description.
Rito::RSO.client_id # default: ENV["RIOT_RSO_CLIENT_ID"]
Rito::RSO.client_secret # default: ENV["RIOT_RSO_CLIENT_SECRET"]
Rito::RSO.client_assertion # default: ENV["RIOT_RSO_CLIENT_ASSERTION"]
Rito::RSO.private_key
Rito::RSO.redirect_uri # default: ENV["RIOT_RSO_REDIRECT_URI"]
Rito::RSO.scope # default: "openid" (add cpid / offline_access)Per-instance kwargs override the module config. Token requests are never retried (authorization codes and refresh tokens are one-time), so handle failures explicitly.
Endpoints declare whether they are platform-routed (na1, kr, ...) or
regionally routed (americas, europe, asia, sea). You pass whichever you
have; the client escalates automatically:
client.summoner.by_puuid(puuid, region: :kr) # platform host: kr
client.matches.ids_by_puuid(puuid, region: :kr) # regional host: asia (escalated)
client.account.by_puuid(puuid, region: :sea) # account-v1 has no SEA host → asiaThe default AdaptiveLimiter learns your key's limits from
X-App-Rate-Limit / X-Method-Rate-Limit headers, syncs counters from
*-Count headers, throttles pre-emptively, and handles 429s:
Retry-Afterpresent → blocks that long (app-scope blocks the whole key+region; method-scope blocks only the endpoint).Retry-Aftermissing → exponential backoff with jitter.- No
X-Rate-Limit-Typeheader → service-level 429 → endpoint-only backoff.
Exhausted retries raise Rito::RateLimited (with .retry_after and
.limit_type). Opt out with Rito::RateLimiting::NullLimiter.
Each process using the default AdaptiveLimiter tracks limits independently,
so a multi-worker setup (Puma cluster, multiple hosts) can collectively exceed
one key's limits. Use the Redis limiter for shared state:
limiter = Rito::RateLimiting::RedisLimiter.new(redis: Redis.new)
client = Rito::Client.new(rate_limiter: limiter)acquire! blocks the calling thread while throttled, so use background jobs
for work that must not block a web request. max_retries: 0 disables retries;
it does not disable pre-emptive waiting. NullLimiter disables limiter-based
waiting entirely.
gem "redis" # soft dependency
limiter = Rito::RateLimiting::RedisLimiter.new(redis: Redis.new)
client = Rito::Client.new(rate_limiter: limiter)Rito::Error
├── BadRequest (400) ├── MethodNotAllowed (405)
├── Unauthorized (401) ├── UnsupportedMediaType (415)
├── Forbidden (403) ├── RateLimited (429)
├── NotFound (404) ├── ServerError / ServiceUnavailable (5xx)
└── ConnectionError (TimeoutError, SSLError)
Every error carries .status and .response. Messages include the method,
URL, and Riot's error detail.
Account, summoner, LoL match, mastery, rotation, and league responses have
Data models. Their mapped fields and .raw payloads are recursively frozen;
unknown fields Riot adds later are preserved in .raw. Other endpoints return
parsed JSON hashes, arrays, or scalar values, including timelines, status,
Clash, challenges, tournaments, and most TFT, VALORANT, and LoR responses:
entry = client.leagues.entries_by_puuid(puuid).first
entry.tier # => "DIAMOND"
entry.winrate # => 57.14
entry.raw # => full payload hashLeague listings require a queue, tier, and division:
client.leagues.entries("RANKED_SOLO_5x5", "DIAMOND", "I", page: 1)| Accessor | API |
|---|---|
client.account |
account-v1 (accounts, active shards, region, me for RSO) |
client.summoner |
summoner-v4 (by_puuid, me for RSO) |
client.matches |
match-v5 (matches, matchlist, timeline, replays) |
client.rso_matches |
lol-rso-match-v1 (RSO bearer token; player resolved from the token) |
client.champion_masteries |
champion-mastery-v4 |
client.champions |
champion-v3 (rotations) |
client.leagues / client.league_exp |
league-v4 / league-exp-v4 |
client.spectator |
spectator-v5 (active_game returns nil on 404) |
client.lol_status |
lol-status-v4 |
client.clash |
clash-v1 |
client.challenges |
lol-challenges-v1 |
client.tournaments / client.tournament_stub |
tournament-v5 / stub-v5 |
client.tft.summoner / .leagues / .matches / .status / .spectator |
tft-summoner-v1, tft-league-v1, tft-match-v1, tft-status-v1, spectator-tft-v5 |
client.val.content / .matches / .console_matches / .ranked / .console_ranked / .status |
val-content-v1, val-match-v1, val-console-match-v1, val-ranked-v1, val-console-ranked-v1, val-status-v1 |
client.lor.matches / .ranked / .status / .decks / .inventory |
lor-match-v1, lor-ranked-v1, lor-status-v1, lor-deck-v1 / lor-inventory-v1 (RSO) |
client.riftbound |
riftbound-content-v1 |
Note: VALORANT uses its own platform routing values (na, eu, ap, kr, latam, br; console: na, eu, ap, br, latam) — the client validates
against the right set per product. esports is a valid platform for
VALORANT content/match and a valid regional for tft-match-v1 (with
esportseu); apac is a valid regional for lor-match-v1. The
tournament endpoints only exist on the americas
host — pass region: :na1 or :americas. Other regional clusters are rejected.
Rito plays well with WebMock. Recorded fixtures via VCR:
bundle exec rake cassettes # re-record (needs RIOT_API_KEY_TEST in .env)
bundle exec rake test
If ActiveSupport is present (e.g. in a Rails app), each Rito::Client request
emits one request.rito notification after it completes, including failures
and bearer requests. attempts counts network attempts across transport and
429 retries; duration_ms includes retry and limiter waits. Configuration and
routing errors that prevent a request do not emit an event:
ActiveSupport::Notifications.subscribe("request.rito") do |*, payload|
Rails.logger.info(
"#{payload[:http_method]} #{payload[:url]} -> #{payload[:status]} " \
"in #{payload[:duration_ms]}ms (attempts: #{payload[:attempts]})"
)
endWithout ActiveSupport, instrumentation is a no-op.
bundle install
bundle exec rake lint
bundle exec rake test
Ruby 3.2+ required (Data.define). Redis tests use REDIS_URL when set and
fail if that configured Redis is unavailable; otherwise they try localhost
and skip if it is unavailable.
The endpoint contract test uses a checked-in snapshot of riotapi-schema
(paths, routing hosts, and query parameters). Refresh it with
bundle exec rake api_routes, review the diff, and run the suite.
Live read-only checks use bundle exec ruby scripts/live_matrix.rb with
RIOT_API_KEY_TEST and RIOT_API_KEY_PROD in .env. Access varies by key and
product: a 403 can indicate missing product access or an incorrect path/host,
as well as an expired key. API keys cannot validate RSO-only endpoints;
those need an RSO access token. Synthetic-id 404s establish no successful
payload coverage.
MIT