Getting Started
How to call Iodid APIs — baseUrl, validation, errors, pagination and filters.
BaseUrl and shard
LoL APIs are under:
https://api.safjnest.com/api/lol
Most platform-scoped endpoints add /{shard}, validated by LolApiParameters.requiredShard (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:163). Search uses /search?q=... across all active shards by default; add &shard=EUW1 to limit it to one shard. Active values defined in LeagueShardUtils.getActives (src/main/java/com/safjnest/lol/utils/LeagueShardUtils.java:64):
EUW1, NA1, KR, EUN1, JP1, BR1, LA1, LA2, TR1, RU, OC1, VN2, SG2, TW2, ME1
Examples: GET /api/lol/search?q=Faker%23EUW searches all shards; GET /api/lol/search?q=Faker%23EUW&shard=EUW1 limits the search (src/main/java/com/safjnest/spring/controller/LolController.java:47).
curl "https://api.safjnest.com/api/lol/search?q=Player%23EUW"
curl "https://api.safjnest.com/api/lol/search?q=Player%23EUW&shard=EUW1"
No authentication required. The APIs are public; protection is only via parameter validation and internal rate limiting via RiotScheduler.
Parameter validation — LolApiParameters
Every query/path parameter goes through LolApiParameters (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:27):
| Method | File:line | Rule |
|---|---|---|
requiredShard | :163 | shard non-empty, must be a valid LeagueShard, UNKNOWN rejected |
requiredText | :230 | trim, non-empty — used for puuid, gameName, tagLine, matchId |
rank / requiredRank | :37, :42 | TierType enum, rank optional, requiredRank required |
queue / optionalQueue | :47, :56 | GameQueueType, default TEAM_BUILDER_RANKED_SOLO (:29), ALL equals null |
role + validateRole | :168, :177 | LaneType among TOP,JUNGLE,MID,BOT,UTILITY (LaneTypeUtils.playables), rejected if queue has no lanes (GameQueueTypeUtils.hasLane :30) |
patch | :103 | regex \d+\.\d+ — major.minor form |
page / limit / matchLimit / matchOffset | :198, :203, :210, :217 | page >=1, leaderboard limit 1..50, match limit 1..100, offset >=0 |
matchOrder | :222 | timeStart:asc or timeStart:desc (default desc) |
activityFilter | :146 | coherent start/end period, champion >=0, uses Filter.canonical() or Filter.summoner() |
otpChampion | :183 | integer >0 |
recordMetric | :193 | required RecordMetric enum |
Validation errors throw ResponseStatusException with 400 BAD_REQUEST via invalid (src/main/java/com/safjnest/spring/controller/LolApiParameters.java:235) and are mapped by LolApiExceptionHandler.
Error envelope — LolApiError
// src/main/java/com/safjnest/spring/dto/LolApiError.java:5
public record LolApiError(int status, String code, String message, ResponseMetadata metadata) {}
LolApiError is the sole Spring DTO for errors (ADR-0005/0007). Fields:
status— numeric HTTP statuscode— application code (e.g.profile_pending,match_pending,profile_activity_pending)message— human-readable descriptionmetadata— optionalResponseMetadatawithpagination,lastUpdate,refresh,filter(src/main/java/com/safjnest/lol/model/ResponseMetadata.java:3)
LolApiExceptionHandler (src/main/java/com/safjnest/spring/controller/LolApiExceptionHandler.java:23) translates ResponseStatusException, MissingServletRequestParameterException, MethodArgumentTypeMismatchException into LolApiError JSON.
ApiResult — 200 / 202 / 404
Services return ApiResult<T> (src/main/java/com/safjnest/lol/model/ApiResult.java:3):
public enum Status { READY, PARTIAL, PENDING, NOT_FOUND }
HTTP mapping in LolApiResponses.from (src/main/java/com/safjnest/spring/controller/LolApiResponses.java:14):
| Status | HTTP | Body |
|---|---|---|
READY / PARTIAL | 200 OK | direct payload (e.g. SummonerView, ProfileActivity) |
PENDING | 202 ACCEPTED | LolApiError with pending code + metadata.refresh=true |
NOT_FOUND | 404 NOT_FOUND | ResponseStatusException with message |
Example pending response (GET /api/lol/EUW1/profile/{puuid} when not yet ready — src/main/java/com/safjnest/spring/controller/LolController.java:291):
{
"status": 202,
"code": "profile_pending",
"message": "Profile initialization is pending",
"metadata": { "lastUpdate": null, "refresh": true, "filter": { "...": "..." } }
}
The profile also includes header X-Profile-Refresh: true when metadata.refresh is true (src/main/java/com/safjnest/spring/controller/LolController.java:294).
ResponseMetadata (src/main/java/com/safjnest/lol/model/ResponseMetadata.java:22) contains Pagination (page, pageSize, limit, offset, total, pages, hasMore) when applicable.
Pagination
Two styles, both with LolApiParameters validation:
Leaderboard — page + limit (LeaderboardController.java:40):
curl "https://api.safjnest.com/api/lol/EUW1/leaderboard?queue=RANKED_SOLO_5X5&rank=DIAMOND&page=2&limit=20"
# limit 1..50, page >=1
Response LeaderboardPage with ResponseMetadata.pagination (src/main/java/com/safjnest/lol/model/ResponseMetadata.java:22).
Match history — offset + limit + sort (LolController.java:91):
curl "https://api.safjnest.com/api/lol/EUW1/profile/puuid/matches?limit=20&offset=0&sort=timeStart:desc&queue=RANKED_SOLO_5X5"
# limit 1..100, offset >=0, sort timeStart:asc|desc
The controller builds MatchPage with hasMore (src/main/java/com/safjnest/lol/service/MatchService.java:244) and ResponseMetadata with pagination (src/main/java/com/safjnest/spring/controller/LolController.java:123).
Canonical filter
Filter (src/main/java/com/safjnest/lol/model/Filter.java:13) represents the combination champion, lane, queue, rank, patch, region, opponent, duo, timeStart, timeEnd.
Filter.canonical()(src/main/java/com/safjnest/lol/model/Filter.java:44) — current-season filter, zeroes champion/lane/queue/rank/patch/region, period =SeasonUtils.getCurrentSeasonRange()(src/main/java/com/safjnest/lol/utils/SeasonUtils.java:41).Filter.summoner(timeStart, timeEnd)(src/main/java/com/safjnest/lol/model/Filter.java:31) — filter for profile/activity, explicit period, other fields zeroed.Filter.activityFilter(start, end, queue, champion)(LolApiParameters.java:146) — validated factory forGET /profile/{puuid}/activity.
Filters are serialized as URL-safe base64 keys toSummonerKey() (src/main/java/com/safjnest/lol/model/Filter.java:283) via FilterCodec.encodeUrl (src/main/java/com/safjnest/lol/utils/FilterCodec.java:15). Used as Redis key for statistics, activity, matchups (ProfileService.java:341).
Complete examples
curl
# Search across every shard, or limit it with `shard`
curl "https://api.safjnest.com/api/lol/search?q=Hide%20on%20bush%23KR1"
curl "https://api.safjnest.com/api/lol/search?q=Hide%20on%20bush%23KR1&shard=EUW1"
# Profile by puuid
curl "https://api.safjnest.com/api/lol/EUW1/profile/abc-123-puuid"
# Profile by Riot ID
curl "https://api.safjnest.com/api/lol/EUW1/profile-by-name/Hide%20on%20bush/KR1"
# Match detail (202 if queued)
curl "https://api.safjnest.com/api/lol/EUW1/match/EUW1_1234567890"
# Activity with time filter
curl "https://api.safjnest.com/api/lol/EUW1/profile/puuid/activity?start=0&end=0&queue=RANKED_SOLO_5X5&champion=0"
# Matchups
curl "https://api.safjnest.com/api/lol/EUW1/profile/puuid/matchups?queue=RANKED_SOLO_5X5&role=MID&minGames=5"
# Champion tier list
curl "https://api.safjnest.com/api/lol/champion/tier-list?patch=15.6&rank=EMERALD®ion=EUW1&queue=RANKED_SOLO_5X5"
# Global records
curl "https://api.safjnest.com/api/lol/records/overview?region=EUW1"
curl "https://api.safjnest.com/api/lol/records/page?metric=KILLS®ion=EUW1&limit=20&offset=0"
JavaScript (fetch)
const baseUrl = "https://api.safjnest.com/api/lol";
async function getProfile(shard, puuid) {
const res = await fetch(`${baseUrl}/${shard}/profile/${puuid}`);
if (res.status === 202) {
const err = await res.json(); // LolApiError
console.log("pending:", err.code, err.metadata);
return { pending: true, ...err };
}
if (res.status === 404) throw new Error("Profile not found");
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const view = await res.json(); // SummonerView
const needsRefresh = res.headers.get("X-Profile-Refresh") === "true";
return { pending: false, view, needsRefresh };
}
async function search(query, shard) {
const params = new URLSearchParams({ q: query, ...(shard ? { shard } : {}) });
const res = await fetch(`${baseUrl}/search?${params}`);
if (!res.ok) throw new Error(await res.text());
return res.json(); // List<SummonerView>
}
async function getMatches(shard, puuid, { limit = 20, offset = 0, sort = "timeStart:desc" } = {}) {
const url = `${baseUrl}/${shard}/profile/${puuid}/matches?limit=${limit}&offset=${offset}&sort=${sort}`;
const res = await fetch(url);
return res.json(); // MatchPage with metadata.pagination
}
Always handle 202 as "data being prepared" — retry after a short interval or show metadata.refresh state.
