{"openapi":"3.0.0","info":{"title":"moddex api","version":"1.0","description":"<p>The public api behind <a href=\"https://moddex.tv\" target=\"_blank\">moddex.tv</a>. Look up the mods, vips and founders in a tracked channel, or the channels a given account holds one of those roles in. Badges are here too.</p>"},"servers":[{"url":"/","description":"this server"},{"url":"https://api.moddex.tv","description":"production"}],"x-moddex-notes":[{"title":"Rate limits","body":"<ul><li>120 requests per 60 seconds, counted against your address.</li><li>With a token: 1200 per 60 seconds, counted against the token.</li><li>Re-index has its own bucket: 20 per 60 seconds.</li><li>Every reply carries <code>RateLimit-Limit</code>, <code>RateLimit-Remaining</code> and <code>RateLimit-Reset</code>, in seconds.</li><li>Over the limit: <code>429</code> with <code>Retry-After</code>.</li><li><code>/health</code> is not limited.</li></ul>"},{"title":"Tokens","body":"<ul><li>Send as <code>Authorization: Bearer &lt;token&gt;</code>.</li><li>Raises the rate limit to 1200 per 60 seconds.</li><li>Opens <code>POST /v1/channels/{login}/reindex</code>.</li><li>Grants no extra visibility. An opted-out account stays a flat <code>404</code>.</li><li>Issued on request, shown once.</li><li>Revoking takes effect on the next request.</li><li>Ask in the <a href=\"https://api.moddex.tv/about\" target=\"_blank\">Discord</a> for one.</li></ul>"},{"title":"Caching","body":"<ul><li>Public reads carry <code>Cache-Control: public, max-age=…</code> and an <code>ETag</code>.</li><li>Send the ETag back as <code>If-None-Match</code> for a <code>304</code>.</li><li>The max-age differs per route.</li><li><code>/v1/chat-badges</code> is uncached.</li></ul>"},{"title":"Pagination is opt-in","body":"<ul><li>No <code>limit</code> and no <code>cursor</code>: the body is a bare array.</li><li>Either one: <code>{ items, limit, hasMore, cursor }</code>.</li><li><code>total</code> comes on the first page only. <code>null</code> afterwards means unchanged.</li><li>Keep the other filters identical while paging.</li></ul>"},{"title":"A 404 tells you nothing","body":"<ul><li>An opted-out account and an unknown account return the same <code>404</code>.</li><li>The body reads <code>not in the moddex index</code>.</li><li>A token does not change this.</li></ul>"},{"title":"A segment is a login, never an id","body":"<ul><li><code>/v1/users/8030</code> is the account named 8030.</li><li>Ids go to <code>/v1/users/by-id/{id}</code>.</li><li>Every reserved segment carries a hyphen.</li></ul>"}],"tags":[{"name":"roles","description":"Tracked mods, vips and founders"},{"name":"users","description":"One tracked account, from either side"},{"name":"browse","description":"Paged lists of indexed channels and accounts"},{"name":"badges","description":"Badge definitions and who wears them"},{"name":"stats","description":"Aggregate counts"},{"name":"eventsub","description":"Whether live role updates are being received"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"<code>Authorization: Bearer &lt;token&gt;</code>. Everything on this page is open and needs no token. A token raises the rate limit to 1200 per 60 seconds and opens <code>POST /v1/channels/{login}/reindex</code>. It grants no extra visibility: an opted-out account stays a flat 404."}},"schemas":{"Badge":{"type":"object","required":["id","slug","name","svg"],"description":"A badge as it hangs off an account. Lean on purpose: this rides on every badge of every user of every list, so it carries the one image a row draws. GET /v1/badges is the catalogue and carries everything.","properties":{"id":{"type":"integer","example":3},"slug":{"type":"string","example":"top-donator","description":"The stable key. Match on this, never on name."},"name":{"type":"string","example":"top donator","description":"Display text, and rewordable. Not a key."},"svg":{"type":"string","example":"https://moddex.tv/badges/top-donator/icon.svg"}}},"IconUrls":{"type":"object","required":["svg","png"],"description":"The badge's own face — a crown, a heart, a wrench. The website draws it from 16px to 46px and always from the vector; the png exists for consumers that cannot take one.","properties":{"svg":{"type":"string","example":"https://moddex.tv/badges/top-donator/icon.svg"},"png":{"type":"string","example":"https://moddex.tv/badges/top-donator/icon.png"}}},"ChatUrls":{"type":"object","required":["1","2","4"],"description":"The moddex mark — a different drawing from the icon, not a smaller one. Keyed the way FrankerFaceZ keys its urls: 1 is 18px, 2 is 36px, 4 is 72px, and this object can be handed to it unchanged.","properties":{"1":{"type":"string","example":"https://moddex.tv/badges/top-donator/1x.webp"},"2":{"type":"string","example":"https://moddex.tv/badges/top-donator/2x.webp"},"4":{"type":"string","example":"https://moddex.tv/badges/top-donator/4x.webp"}}},"BadgeImages":{"type":"object","required":["icon","chat"],"description":"Both drawings. Always present, wearable or not.","properties":{"icon":{"$ref":"#/components/schemas/IconUrls"},"chat":{"$ref":"#/components/schemas/ChatUrls"}}},"BadgeCatalogueEntry":{"type":"object","required":["id","slug","name","chatName","order","wearable","images"],"description":"One badge, everything about it.","properties":{"id":{"type":"integer","example":3},"slug":{"type":"string","example":"top-donator"},"name":{"type":"string","example":"top donator"},"chatName":{"type":"string","nullable":true,"example":"moddex top donator","description":"What a chat client renders, with the vendor prefix. Null when the badge cannot be worn."},"order":{"type":"integer","nullable":true,"example":30,"description":"Where it sits in a badge row, read left to right."},"wearable":{"type":"boolean","example":true,"description":"Whether somebody may wear this in chat. Four of the nine may not, and they still carry every image."},"images":{"$ref":"#/components/schemas/BadgeImages"}}},"ChatBadge":{"type":"object","required":["slug","name","images"],"description":"The chat mark somebody is wearing, or may choose to.","properties":{"slug":{"type":"string","example":"top-donator"},"name":{"type":"string","example":"moddex top donator"},"images":{"$ref":"#/components/schemas/ChatUrls"}}},"ChatBadgeGroup":{"type":"object","required":["slug","name","images","users"],"description":"A wearable badge and everybody wearing it. The roster is a list of these, and it is read over http — moddex-ws carries changes to it, never the list itself.","properties":{"slug":{"type":"string","example":"top-donator"},"name":{"type":"string","example":"moddex top donator"},"images":{"$ref":"#/components/schemas/ChatUrls"},"users":{"type":"array","items":{"type":"string"},"description":"The ids of every account wearing this badge.","example":["401235515"]}}},"User":{"type":"object","required":["id","login","name","avatar","badges","bio","created"],"properties":{"id":{"type":"string","example":"217986157"},"login":{"type":"string","example":"maersux"},"name":{"type":"string","example":"maersux","description":"Twitch display name."},"avatar":{"type":"string","example":"https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png"},"follower":{"type":"integer","example":317},"discord":{"type":"string","example":"780910551286546493"},"bot":{"type":"boolean","example":false,"description":"Set from the curated bot list. Bots dominate large mod lists."},"banned":{"type":"string","example":"","description":"Twitch's reason, or an empty string if not banned."},"chatBadge":{"oneOf":[{"$ref":"#/components/schemas/ChatBadge"},{"type":"null"}]},"badges":{"type":"array","items":{"$ref":"#/components/schemas/Badge"}},"bio":{"type":"string","example":"this random dev guy. if there's a social media platform with this name, it's probably me.","description":"The Twitch account bio."},"created":{"type":"string","example":"2018-05-01T18:09:47.000Z","description":"When the Twitch account was created."},"updated":{"type":"string","example":"2024-01-01T00:00:00.000Z","description":"When this account was last read as a channel — its roles. It is not a freshness stamp for the fields around it."}}},"Role":{"type":"object","required":["id","login","name","avatar","follower","bot","badges","granted"],"description":"A role row carries less than a User: the role queries read no bio, no socials and no chat badge.","properties":{"id":{"type":"string","example":"217986157"},"login":{"type":"string","example":"maersux"},"name":{"type":"string","example":"maersux"},"avatar":{"type":"string","example":"https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png"},"follower":{"type":"integer","example":317,"description":"Under `sort=followers` this is the figure the list is ordered by, which is the count read when the role was last scraped rather than the live one."},"bot":{"type":"boolean","example":false,"description":"Set from the curated bot list. Bots dominate large mod lists."},"chatBadge":{"oneOf":[{"$ref":"#/components/schemas/ChatBadge"},{"type":"null"}],"description":"Always null on this endpoint — role lists do not read chat badges."},"badges":{"type":"array","items":{"$ref":"#/components/schemas/Badge"}},"granted":{"type":"string","nullable":true,"example":"2023-01-01T00:00:00.000Z","description":"when the role was granted, or null when twitch gave no date — a moderator handed over at connect time carries no date at all. permanent: for founders this is the entitlement date, not whether the badge is displayed today."}}},"RolePage":{"type":"object","required":["items","limit","hasMore","cursor","total"],"description":"The body a role list answers with once `limit` or `cursor` is sent.","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/Role"}},"limit":{"type":"integer","example":100},"hasMore":{"type":"boolean","example":true},"cursor":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Send back as `cursor` for the next page. It only comes while `hasMore` is true, and it is tied to the `sort` and `dir` it was made with."},"total":{"oneOf":[{"type":"integer"},{"type":"null"}],"example":97450,"description":"The size of the whole list, on the FIRST page only. A null on a later page means unchanged, not unknown."}}},"RoleCounts":{"type":"object","required":["mod","vip","founder","total"],"description":"Which direction these count depends on the endpoint: on /v1/channels they are the roles held IN the channel, on /v1/users the roles this account holds ELSEWHERE.","properties":{"mod":{"type":"integer","example":12},"vip":{"type":"integer","example":3},"founder":{"type":"integer","example":0},"total":{"type":"integer","example":15}}},"BrowsePage":{"type":"object","required":["items","limit","offset","hasMore"],"description":"The body both browse endpoints answer with.","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/User"},"description":"Each entry is a User carrying one extra field, `counts` (RoleCounts)."},"limit":{"type":"integer","example":25},"offset":{"type":"integer","example":0},"hasMore":{"type":"boolean","example":true,"description":"Read one row past the page rather than counted, so it costs nothing."}}},"Stats":{"type":"object","required":["channels","users","mods","vips","founders","takenAt"],"properties":{"channels":{"type":"integer","example":1220000},"users":{"type":"integer","example":8200000},"mods":{"type":"integer","example":6100000},"vips":{"type":"integer","example":900000},"founders":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"null means not counted at the time of this snapshot, not none."},"takenAt":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"2026-08-15T03:00:00.000Z","description":"When the snapshot was taken. null on an install that has not reached its first 03:00 UTC."}}},"StatsPoint":{"type":"object","required":["day","channels","users","mods","vips","founders"],"properties":{"day":{"type":"string","example":"2026-08-15"},"channels":{"type":"integer","example":1220000},"users":{"type":"integer","example":8200000},"mods":{"type":"integer","example":6100000},"vips":{"type":"integer","example":900000},"founders":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"null for days before founders were measured. Break the line, do not draw a zero."}}},"TopDonator":{"type":"object","required":["rank","userId","login","displayName","totalCents","payments","latest"],"properties":{"rank":{"type":"integer","example":1},"userId":{"type":"string","example":"217986157"},"login":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"maersux","description":"null while the donating account has never been fetched into the database."},"displayName":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"maersux"},"totalCents":{"type":"integer","example":2500,"description":"Cents, so no currency arithmetic happens in json."},"payments":{"type":"integer","example":3},"latest":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"2026-08-01T12:00:00.000Z"}}},"Grant":{"type":"object","required":["grantedAt"],"description":"A role somebody holds. Absent from a membership answer as null.","properties":{"grantedAt":{"type":"string","nullable":true,"example":"2025-02-09T17:22:48.000Z","description":"When twitch says it was granted. Null when twitch gave no date."}}},"Membership":{"type":"object","required":["mod","vip","founder"],"description":"Whether one account holds each role in one channel. Every role is a key, so a null is an answer rather than an omission.","properties":{"mod":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Grant"}]},"vip":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Grant"}]},"founder":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/Grant"}]}}},"ChannelRoles":{"type":"object","required":["mods","vips","founders"],"description":"All three of a channel's role lists in one answer. A channel's list is small — 225 rows at the largest, 3.2 on average — so three calls would be silly. A user has no such route: one of those lists is 590,787 rows.","properties":{"mods":{"type":"array","items":{"$ref":"#/components/schemas/Role"}},"vips":{"type":"array","items":{"$ref":"#/components/schemas/Role"}},"founders":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}}},"LeaderRow":{"type":"object","required":["place","count","id","login","name"],"description":"One row of a board.","properties":{"place":{"type":"integer","example":1,"description":"The position in the list you asked for, counted from 1 with no gaps. Filtering renumbers it, so it is not the same thing as an account rank."},"moved":{"type":"integer","nullable":true,"example":2,"description":"How many places this account gained since the reference board, negative if it lost them. Null when that board did not carry it. Measured in PLACE under the same bots filter, so it agrees with the number beside it."},"count":{"type":"integer","example":905753},"id":{"type":"string","example":"19264788"},"login":{"type":"string","example":"nightbot"},"name":{"type":"string","example":"Nightbot"},"avatar":{"type":"string"},"bot":{"type":"boolean","example":true},"badges":{"type":"array","items":{"$ref":"#/components/schemas/Badge"}}}},"Leaderboard":{"type":"object","required":["role","computedAt","depth","of","items"],"description":"Who holds the most of one role. Built nightly into a table rather than indexed live — the rank columns carry no index on purpose.","properties":{"role":{"type":"string","example":"mod"},"computedAt":{"type":"string","example":"2026-08-26T04:00:00.000Z","description":"Copied from the rollup, never defaulted, so both endpoints agree."},"depth":{"type":"integer","example":1000,"description":"How deep the board is materialised. Past it there is nothing to serve."},"of":{"type":"integer","example":6130351,"description":"How many accounts hold at least one of this role."},"comparedTo":{"type":"string","nullable":true,"example":"2026-08-19","description":"The board every `moved` is measured against: the newest one at or before seven days back, so a night the job missed shifts the comparison rather than emptying it. Null while no such board exists."},"items":{"type":"array","items":{"$ref":"#/components/schemas/LeaderRow"}}}},"EventsubHealth":{"type":"object","required":["status","service","eventsub"],"description":"Public and cached, so moddex-status can watch live role updates without holding a secret.","properties":{"status":{"type":"string","example":"ok"},"service":{"type":"string","example":"moddex-api"},"eventsub":{"type":"string","example":"receiving"},"enabledShards":{"type":"integer","example":1},"totalShards":{"type":"integer","example":1,"description":"One shard, deliberately. More than one reports itself as a problem."}}},"Error":{"type":"object","required":["error","status"],"properties":{"error":{"type":"string","description":"the status text, always","example":"not found"},"status":{"type":"integer","description":"http status code","example":404},"message":{"type":"string","description":"optional detail, for humans","example":"no badge with id 9"}}}}},"paths":{"/v1/users/{login}":{"get":{"tags":["users"],"description":"One account: its Twitch fields, the badges it wears, and how many roles of\neach kind it holds with its `rank` and the population that rank is out of.\n\nAn account holding none of a role has `rank: null`. Add `?include=roles` for\nthe first page of each role list.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The twitch login. This segment is **always** a login, never an id —\nsome logins are all digits, so `8030` is the account named `8030`.\nIds go to `/v1/users/by-id/{id}`.\n","schema":{"type":"string"}},{"name":"include","in":"query","description":"`roles` adds the first page of each role list.\n","schema":{"type":"string","enum":["roles"]}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"No such account — and the same answer, byte for byte, for an account\nthat has opted out. A distinguishable reply would turn this endpoint\ninto a list of who opted out, which is the one fact the opt-out\nexists to keep private.\n"}}}},"/v1/users/by-id/{id}":{"get":{"tags":["users"],"description":"The same account, addressed by its Twitch id.\n","parameters":[{"name":"id","in":"path","required":true,"description":"The numeric twitch id.","schema":{"type":"string"}},{"name":"include","in":"query","description":"As on the login form.","schema":{"type":"string","enum":["roles"]}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"no such account"}}}},"/v1/users/{login}/roles/{channel}":{"get":{"tags":["roles"],"description":"Does this account hold a role in that channel? Answers every role at once,\neach either `null` or the date it was granted.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The account, by login.","schema":{"type":"string"}},{"name":"channel","in":"path","required":true,"description":"The channel, by login.","schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Membership"}}}},"404":{"description":"no such account, or no such channel"}}}},"/v1/channels/{login}":{"get":{"tags":["users"],"description":"The same Twitch account seen from the other side: what it has **granted**\nrather than what it holds.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The channel's twitch login. Ids go to `/v1/channels/by-id/{id}`.","schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"no such channel"}}}},"/v1/channels/by-id/{id}":{"get":{"tags":["users"],"description":"The same channel, addressed by its Twitch id.\n","parameters":[{"name":"id","in":"path","required":true,"description":"The numeric twitch id.","schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}},"404":{"description":"no such channel"}}}},"/v1/channels/{login}/roles":{"get":{"tags":["roles"],"description":"Every role this channel has granted — mods, vips and founders in one answer.\nFor a single role, or to page it, use `/v1/channels/{login}/{role}`.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The channel's twitch login.","schema":{"type":"string"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelRoles"}}}},"404":{"description":"no such channel"}}}},"/v1/leaderboards/{role}":{"get":{"tags":["stats"],"description":"Who holds the most roles of one kind. `mods`, `vips` and `founders` count\nthat role alone; `roles` counts all three together.\n\nRebuilt once a day, so `computedAt` is how old the board is. Only the first\n`depth` places exist.\n\n`place` is the position in the list you asked for, counted from 1 with no\ngaps — so `bots=exclude` renumbers what is left, and an `after` taken under\none filter is meaningless under another.\n\n`moved` is how far each account came since `comparedTo`, seven days back,\nand it is measured in `place` under the same filter. It is null for anyone\nthat board did not carry.\n","parameters":[{"in":"path","name":"role","required":true,"schema":{"type":"string","enum":["mods","vips","founders","roles"]}},{"in":"query","name":"limit","schema":{"type":"integer"},"description":"how many places to return, 10 by default and 100 at most"},{"in":"query","name":"after","schema":{"type":"integer"},"description":"Resume after this position — the `after` of the previous page. Only\nvalid for the same `bots` setting the page was built with.\n"},{"in":"query","name":"bots","schema":{"type":"string","enum":["include","exclude"]},"description":"`include` by default. The mod and vip boards are almost entirely\nchat bots, so a human-facing list wants `exclude`.\n"}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Leaderboard"}}}},"400":{"description":"a malformed limit, after or bots value"}}}},"/v1/stats":{"get":{"tags":["stats"],"description":"The most recent counts of tracked channels, accounts, mods, vips and\nfounders, with the time they were taken.\n\nTaken once a day, so this is a snapshot rather than a live count. Before the\nfirst one it answers zeroes and a null `takenAt`.\n","responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stats"}}}}}}},"/v1/stats/history":{"get":{"tags":["stats"],"description":"One point per day, oldest first, ready to chart. `days` defaults to 30 and is\nclamped to 1..365.\n\nA day holding several rows collapses to its last one. `founders` is `null`\nfor days before it was measured — that means \"not counted\", not \"none\", so\nbreak the line there rather than drawing a zero.\n","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":365,"default":30}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StatsPoint"}}}}}}}},"/v1/search":{"get":{"tags":["browse"],"description":"Accounts whose login starts with `q`, best-followed first. This is the\nlookup behind the search bar, not a way to enumerate the index: it\nmatches the **start** of a login only, never a display name, and it\nanswers at most 25 rows with no cursor.\n\nRanking is bounded rather than exhaustive, and it reads two sources. The\nlogin typed in full always wins. After it come the best-followed matches\namong the accounts at the head of the follower scale, which is\nmaterialised nightly — that is what puts a large account in front of a\ncommon prefix. The rest of the answer is filled from the first `window`\nlogins in alphabetical order, sorted by follower count among themselves.\n\nSo a very common prefix ranks the notable accounts plus the head of its\nalphabet, not the whole match set. `window` is returned so a caller can\ntell the two cases apart. An account that became popular since the last\nnightly rebuild ranks from the alphabetical half until it is picked up.\n\nAccounts that opted out are absent, exactly as they are everywhere else.\n","parameters":[{"name":"q","in":"query","required":true,"description":"the start of a login, 3 characters minimum","schema":{"type":"string"}},{"name":"limit","in":"query","description":"1 to 25, default 8","schema":{"type":"integer"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/User"}},"limit":{"type":"integer"},"window":{"type":"integer"}}}}}},"400":{"description":"q is missing or shorter than 3 characters"}}}},"/v1/channels/{login}/{role}":{"get":{"tags":["roles"],"description":"Who holds a role in this channel. `role` is one of `mods`, `vips` or\n`founders`; anything else answers 404.\n\nThese lists are small, so this answers whole by default. For all three at\nonce use `/v1/channels/{login}/roles`.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The channel's twitch login. This segment is **always** a login, never\nan id — some logins are all digits. Ids go to\n`/v1/channels/by-id/{id}`.\n","schema":{"type":"string"}},{"name":"role","in":"path","required":true,"description":"Which role to list.","schema":{"type":"string","enum":["mods","vips","founders"]}},{"name":"limit","in":"query","description":"Page size, 1–100. **Send this (or `cursor`) and the response switches\nfrom a bare array to `{ items, limit, hasMore, cursor, total }`.**\n","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","description":"The opaque cursor from the previous response. It is tied to the `dir`\nit was made with; replayed against another ordering it is ignored.\n","schema":{"type":"string"}},{"name":"dir","in":"query","description":"Sort direction. This axis always sorts by `granted`.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RolePage"}}}},"404":{"description":"no such channel, or no such role"}}}},"/v1/users/{login}/{role}":{"get":{"tags":["roles"],"description":"The channels where this account holds a role. These lists run to hundreds of\nthousands of rows, so page them with `limit`. The role is required.\n","parameters":[{"name":"login","in":"path","required":true,"description":"The account's twitch login. Always a login, never an id — ids go to\n`/v1/users/by-id/{id}`.\n","schema":{"type":"string"}},{"name":"role","in":"path","required":true,"description":"Which role to list.","schema":{"type":"string","enum":["mods","vips","founders"]}},{"name":"limit","in":"query","description":"Page size, 1–100. **Send this (or `cursor`) and the response switches\nfrom a bare array to `{ items, limit, hasMore, cursor, total }`.**\n\n`total` comes on the first page only; `null` afterwards means it has\nnot changed. On very large lists it is a size rather than an exact\ncount.\n","schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"q","in":"query","description":"Narrow the page to channels whose login **starts with** this term.\nThree characters minimum; shorter answers 400.\n","schema":{"type":"string","minLength":3}},{"name":"cursor","in":"query","description":"The opaque cursor from the previous response's `cursor` field, tied\nto the `sort` and `dir` it was made with.\n","schema":{"type":"string"}},{"name":"sort","in":"query","description":"`granted` (default) or `followers`. A list shows the number it sorts\nby, so under `followers` a row carries the count recorded on the\nrole rather than the account's live one.\n","schema":{"type":"string","enum":["granted","followers"]}},{"name":"dir","in":"query","description":"Sort direction.","schema":{"type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RolePage"}}}},"400":{"description":"a search term shorter than three characters"},"404":{"description":"no such account, or no such role"}}}},"/v1/channels/{login}/reindex":{"post":{"tags":["roles"],"summary":"Queue a channel for a fresh role read","description":"Queues a role read for a channel and returns at once. The read itself takes seconds to minutes. `queued: false` means the channel was already waiting or the queue was full, and is not an error. Needs a token, and uses a smaller rate limit bucket than the rest of the api.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"login","required":true,"schema":{"type":"string"},"description":"the twitch login, never an id. Use the `by-id` twin for an id"}],"responses":{"200":{"description":"accepted, queued or already waiting","content":{"application/json":{"schema":{"type":"object","required":["login","queued"],"properties":{"login":{"type":"string","description":"the channel as moddex spells it","example":"forsen"},"queued":{"type":"boolean","description":"false when it was already waiting, or the queue was full","example":true}}}}}},"400":{"description":"the login is not a twitch login"},"403":{"description":"no token was sent"},"404":{"description":"not in the moddex index","or opted out. The two are the same reply":null},"429":{"description":"over the re-index bucket. Retry-After says how long"}}}},"/v1/channels/by-id/{id}/reindex":{"post":{"tags":["roles"],"summary":"Queue a channel for a fresh role read, by twitch id","description":"The `by-id` twin of the route above. Same token, same bucket, same replies. An id never changes, a login does.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"},"description":"the numeric twitch id"}],"responses":{"200":{"description":"accepted, queued or already waiting","content":{"application/json":{"schema":{"type":"object","required":["login","queued"],"properties":{"login":{"type":"string","example":"forsen"},"queued":{"type":"boolean","example":true}}}}}},"400":{"description":"the id is not a numeric twitch id"},"403":{"description":"no token was sent"},"404":{"description":"not in the moddex index","or opted out. The two are the same reply":null},"429":{"description":"over the re-index bucket. Retry-After says how long"}}}},"/v1/eventsub/health":{"get":{"tags":["eventsub"],"description":"200 while live role updates are being received, 503 when they are not.\nAnswers are cached briefly.\n\nA deployment with eventsub switched off answers 200 — that is a\nconfiguration, not an outage.\n","responses":{"200":{"description":"at least one shard is receiving, or eventsub is off","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsubHealth"}}}},"503":{"description":"no enabled shard","or twitch did not answer":null}}}},"/v1/channels":{"get":{"tags":["browse"],"description":"A page of the Twitch channels with at least one indexed role. Each entry's\n`counts` point **inward**: the mods, vips and founders this channel has.\n\n`sort=roles` reads a figure refreshed once a day, so its ordering can be up\nto that far behind.\n","parameters":[{"name":"sort","in":"query","description":"read (default, most recently scraped first), roles, followers","schema":{"type":"string","enum":["read","roles","followers"]}},{"name":"limit","in":"query","description":"1 to 100, default 25","schema":{"type":"integer"}},{"name":"offset","in":"query","description":"0 to 10000. deep paging is capped on purpose","schema":{"type":"integer"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowsePage"}}}}}}},"/v1/users":{"get":{"tags":["browse"],"description":"A page of the Twitch accounts that hold at least one role — the direction\nTwitch does not expose itself. Each entry's `counts` point **outward**: the\nchannels this account holds a role in.\n\nThe same account can appear on `/v1/channels` with entirely different\nnumbers. `sort=roles` reads a figure refreshed once a day.\n","parameters":[{"name":"sort","in":"query","description":"roles (default), followers","schema":{"type":"string","enum":["roles","followers"]}},{"name":"bots","in":"query","description":"exclude to drop curated bot accounts. included by default","schema":{"type":"string","enum":["include","exclude"]}},{"name":"limit","in":"query","description":"1 to 100, default 25","schema":{"type":"integer"}},{"name":"offset","in":"query","description":"0 to 10000","schema":{"type":"integer"}}],"responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrowsePage"}}}}}}},"/v1/badges":{"get":{"tags":["badges"],"description":"The badge catalogue: every badge, its stable slug, where it sits in a row,\nand both of its drawings — the icon a website renders and the mark a chat\nclient renders, at three densities.\n\n**Every image is present whether or not the badge is wearable.** The mark\nof `affiliate` exists; it is simply not somebody's to wear. `wearable`\nsays whether it may be, and `chatName` is null when it may not.\n\n`images.chat` is keyed the way FrankerFaceZ keys its `urls`, so it can be\nhanded over unchanged.\n","parameters":[{"name":"slug","in":"query","description":"one badge by its stable key — prefer this","schema":{"type":"string"}},{"name":"id","in":"query","description":"one badge by id","schema":{"type":"integer"}},{"name":"name","in":"query","description":"one badge by display name, which is rewordable","schema":{"type":"string"}}],"responses":{"200":{"description":"One badge when `slug`, `id` or `name` is sent, the whole set otherwise,\nordered the way a badge row reads left to right.\n","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/BadgeCatalogueEntry"},{"type":"array","items":{"$ref":"#/components/schemas/BadgeCatalogueEntry"}}]}}}},"400":{"description":"invalid input"},"404":{"description":"badge not found"}}}},"/v1/chat-badges":{"get":{"tags":["badges"],"description":"Every wearable badge with the accounts wearing it. A badge nobody wears\ncomes back with an empty `users` array rather than being left out.\n\n**This is the only place the roster exists.** Read it when your client\nstarts and again whenever it reconnects to `ws.moddex.tv`, which carries\nchanges to this list and never the list itself.\n\nDeliberately not cached: nothing repairs a stale copy, so it must be\ncurrent at the moment a client starts listening. It carries an ETag, so\nan unchanged roster costs a 304.\n","responses":{"200":{"description":"successful operation","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ChatBadgeGroup"}}}}}}}}}}