The public api behind moddex.tv. 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.
Base URL
https://api.moddex.tv
Auth
Authorization: Bearer <token>. Everything on this page is open and needs no token. A token raises the rate limit to 1200 per 60 seconds and opens POST /v1/channels/{login}/reindex. It grants no extra visibility: an opted-out account stays a flat 404.
Before you start
What every endpoint below has in common, so none of it has to be guessed.
Rate limits
120 requests per 60 seconds, counted against your address.
With a token: 1200 per 60 seconds, counted against the token.
Re-index has its own bucket: 20 per 60 seconds.
Every reply carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, in seconds.
Over the limit: 429 with Retry-After.
/health is not limited.
Tokens
Send as Authorization: Bearer <token>.
Raises the rate limit to 1200 per 60 seconds.
Opens POST /v1/channels/{login}/reindex.
Grants no extra visibility. An opted-out account stays a flat 404.
Who holds a role in this channel. role is one of mods, vips or
founders; anything else answers 404.
These lists are small, so this answers whole by default. For all three at
once use /v1/channels/{login}/roles.
Parameters
Name
In
Type
Description
loginrequired
path
string
The channel's twitch login. This segment is always a login, never
an id — some logins are all digits. Ids go to
/v1/channels/by-id/{id}.
rolerequired
path
string
Which role to list.
limit
query
integer
Page size, 1–100. Send this (or cursor) and the response switches
from a bare array to { items, limit, hasMore, cursor, total }.
cursor
query
string
The opaque cursor from the previous response. It is tied to the dir
it was made with; replayed against another ordering it is ignored.
dir
query
string
Sort direction. This axis always sorts by granted.
The channels where this account holds a role. These lists run to hundreds of
thousands of rows, so page them with limit. The role is required.
Parameters
Name
In
Type
Description
loginrequired
path
string
The account's twitch login. Always a login, never an id — ids go to
/v1/users/by-id/{id}.
rolerequired
path
string
Which role to list.
limit
query
integer
Page size, 1–100. Send this (or cursor) and the response switches
from a bare array to { items, limit, hasMore, cursor, total }.total comes on the first page only; null afterwards means it has
not changed. On very large lists it is a size rather than an exact
count.
q
query
string
Narrow the page to channels whose login starts with this term.
Three characters minimum; shorter answers 400.
cursor
query
string
The opaque cursor from the previous response's cursor field, tied
to the sort and dir it was made with.
sort
query
string
granted (default) or followers. A list shows the number it sorts
by, so under followers a row carries the count recorded on the
role rather than the account's live one.
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.
Parameters
Name
In
Type
Description
loginrequired
path
string
the twitch login, never an id. Use the by-id twin for an id
Responses
Code
Body
Description
200
object
accepted, queued or already waiting
400
the login is not a twitch login
403
no token was sent
404
not in the moddex index
429
over the re-index bucket. Retry-After says how long
Response body
{
"login": "forsen",
"queued": true
}
Example request
const res = await fetch('https://api.moddex.tv/v1/channels/<login>/reindex', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
curl -X POST https://api.moddex.tv/v1/channels/<login>/reindex \
-H "Authorization: Bearer $MODDEX_TOKEN"
import requests
res = requests.request("POST", "https://api.moddex.tv/v1/channels/<login>/reindex", headers={"Authorization": f"Bearer {token}"})
data = res.json()
One account: its Twitch fields, the badges it wears, and how many roles of
each kind it holds with its rank and the population that rank is out of.
An account holding none of a role has rank: null. Add ?include=roles for
the first page of each role list.
Parameters
Name
In
Type
Description
loginrequired
path
string
The twitch login. This segment is always a login, never an id —
some logins are all digits, so 8030 is the account named 8030.
Ids go to /v1/users/by-id/{id}.
No such account — and the same answer, byte for byte, for an account
that has opted out. A distinguishable reply would turn this endpoint
into a list of who opted out, which is the one fact the opt-out
exists to keep private.
Response body
{
"id": "217986157",
"login": "maersux",
"name": "maersux",
"avatar": "https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png",
"follower": 317,
"discord": "780910551286546493",
"bot": false,
"banned": "",
"chatBadge": {
"slug": "top-donator",
"name": "moddex top donator",
"images": {
"1": "https://moddex.tv/badges/top-donator/1x.webp",
"2": "https://moddex.tv/badges/top-donator/2x.webp",
"4": "https://moddex.tv/badges/top-donator/4x.webp"
}
},
"badges": [
{
"id": 3,
"slug": "top-donator",
"name": "top donator",
"svg": "https://moddex.tv/badges/top-donator/icon.svg"
}
],
"bio": "this random dev guy. if there's a social media platform with this name, it's probably me.",
"created": "2018-05-01T18:09:47.000Z",
"updated": "2024-01-01T00:00:00.000Z"
}
Example request
// query: include
const res = await fetch('https://api.moddex.tv/v1/users/<login>');
const data = await res.json();
curl https://api.moddex.tv/v1/users/<login>
# query: include
import requests
# query: include
res = requests.get("https://api.moddex.tv/v1/users/<login>")
data = res.json()
Accounts whose login starts with q, best-followed first. This is the
lookup behind the search bar, not a way to enumerate the index: it
matches the start of a login only, never a display name, and it
answers at most 25 rows with no cursor.
Ranking is bounded rather than exhaustive, and it reads two sources. The
login typed in full always wins. After it come the best-followed matches
among the accounts at the head of the follower scale, which is
materialised nightly — that is what puts a large account in front of a
common prefix. The rest of the answer is filled from the first window
logins in alphabetical order, sorted by follower count among themselves.
So a very common prefix ranks the notable accounts plus the head of its
alphabet, not the whole match set. window is returned so a caller can
tell the two cases apart. An account that became popular since the last
nightly rebuild ranks from the alphabetical half until it is picked up.
Accounts that opted out are absent, exactly as they are everywhere else.
Parameters
Name
In
Type
Description
qrequired
query
string
the start of a login, 3 characters minimum
limit
query
integer
1 to 25, default 8
Responses
Code
Body
Description
200
object
successful operation
400
q is missing or shorter than 3 characters
Response body
{
"items": [
{
"id": "217986157",
"login": "maersux",
"name": "maersux",
"avatar": "https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png",
"follower": 317,
"discord": "780910551286546493",
"bot": false,
"banned": "",
"chatBadge": {
"slug": "top-donator",
"name": "moddex top donator",
"images": null
},
"badges": [
{
"id": 3,
"slug": "top-donator",
"name": "top donator",
"svg": "https://moddex.tv/badges/top-donator/icon.svg"
}
],
"bio": "this random dev guy. if there's a social media platform with this name, it's probably me.",
"created": "2018-05-01T18:09:47.000Z",
"updated": "2024-01-01T00:00:00.000Z"
}
],
"limit": 0,
"window": 0
}
Example request
// query: q, limit
const res = await fetch('https://api.moddex.tv/v1/search');
const data = await res.json();
curl https://api.moddex.tv/v1/search
# query: q, limit
import requests
# query: q, limit
res = requests.get("https://api.moddex.tv/v1/search")
data = res.json()
A page of the Twitch channels with at least one indexed role. Each entry's
counts point inward: the mods, vips and founders this channel has.
sort=roles reads a figure refreshed once a day, so its ordering can be up
to that far behind.
Parameters
Name
In
Type
Description
sort
query
string
read (default, most recently scraped first), roles, followers
A page of the Twitch accounts that hold at least one role — the direction
Twitch does not expose itself. Each entry's counts point outward: the
channels this account holds a role in.
The same account can appear on /v1/channels with entirely different
numbers. sort=roles reads a figure refreshed once a day.
Parameters
Name
In
Type
Description
sort
query
string
roles (default), followers
bots
query
string
exclude to drop curated bot accounts. included by default
The badge catalogue: every badge, its stable slug, where it sits in a row,
and both of its drawings — the icon a website renders and the mark a chat
client renders, at three densities.
Every image is present whether or not the badge is wearable. The mark
of affiliate exists; it is simply not somebody's to wear. wearable
says whether it may be, and chatName is null when it may not.
images.chat is keyed the way FrankerFaceZ keys its urls, so it can be
handed over unchanged.
Parameters
Name
In
Type
Description
slug
query
string
one badge by its stable key — prefer this
id
query
integer
one badge by id
name
query
string
one badge by display name, which is rewordable
Responses
Code
Body
Description
200
BadgeCatalogueEntry | BadgeCatalogueEntry[]
One badge when `slug`, `id` or `name` is sent, the whole set otherwise,
ordered the way a badge row reads left to right.
Every wearable badge with the accounts wearing it. A badge nobody wears
comes back with an empty users array rather than being left out.
This is the only place the roster exists. Read it when your client
starts and again whenever it reconnects to ws.moddex.tv, which carries
changes to this list and never the list itself.
Deliberately not cached: nothing repairs a stale copy, so it must be
current at the moment a client starts listening. It carries an ETag, so
an unchanged roster costs a 304.
Who holds the most roles of one kind. mods, vips and founders count
that role alone; roles counts all three together.
Rebuilt once a day, so computedAt is how old the board is. Only the first
depth places exist.
place is the position in the list you asked for, counted from 1 with no
gaps — so bots=exclude renumbers what is left, and an after taken under
one filter is meaningless under another.
moved is how far each account came since comparedTo, seven days back,
and it is measured in place under the same filter. It is null for anyone
that board did not carry.
Parameters
Name
In
Type
Description
rolerequired
path
string
limit
query
integer
how many places to return, 10 by default and 100 at most
after
query
integer
Resume after this position — the after of the previous page. Only
valid for the same bots setting the page was built with.
bots
query
string
include by default. The mod and vip boards are almost entirely
chat bots, so a human-facing list wants exclude.
The most recent counts of tracked channels, accounts, mods, vips and
founders, with the time they were taken.
Taken once a day, so this is a snapshot rather than a live count. Before the
first one it answers zeroes and a null takenAt.
One point per day, oldest first, ready to chart. days defaults to 30 and is
clamped to 1..365.
A day holding several rows collapses to its last one. founders is null
for days before it was measured — that means "not counted", not "none", so
break the line there rather than drawing a zero.
200 while live role updates are being received, 503 when they are not.
Answers are cached briefly.
A deployment with eventsub switched off answers 200 — that is a
configuration, not an outage.
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.
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.
1
moved
integer
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.
Copied from the rollup, never defaulted, so both endpoints agree.
2026-08-26T04:00:00.000Z
depthrequired
integer
How deep the board is materialised. Past it there is nothing to serve.
1000
ofrequired
integer
How many accounts hold at least one of this role.
6130351
comparedTo
string
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.