moddex api1.0

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.
  • Issued on request, shown once.
  • Revoking takes effect on the next request.
  • Ask in the Discord for one.

Caching

  • Public reads carry Cache-Control: public, max-age=… and an ETag.
  • Send the ETag back as If-None-Match for a 304.
  • The max-age differs per route.
  • /v1/chat-badges is uncached.

Pagination is opt-in

  • No limit and no cursor: the body is a bare array.
  • Either one: { items, limit, hasMore, cursor }.
  • total comes on the first page only. null afterwards means unchanged.
  • Keep the other filters identical while paging.

A 404 tells you nothing

  • An opted-out account and an unknown account return the same 404.
  • The body reads not in the moddex index.
  • A token does not change this.

A segment is a login, never an id

  • /v1/users/8030 is the account named 8030.
  • Ids go to /v1/users/by-id/{id}.
  • Every reserved segment carries a hyphen.

roles

Tracked mods, vips and founders

GET /v1/users/{login}/roles/{channel} #

Does this account hold a role in that channel? Answers every role at once, each either null or the date it was granted.

Parameters

NameInTypeDescription
login required path string The account, by login.
channel required path string The channel, by login.

Responses

CodeBodyDescription
200 Membership successful operation
404 no such account, or no such channel
Response body
{
  "mod": {
    "grantedAt": "2025-02-09T17:22:48.000Z"
  },
  "vip": {
    "grantedAt": "2025-02-09T17:22:48.000Z"
  },
  "founder": {
    "grantedAt": "2025-02-09T17:22:48.000Z"
  }
}

Example request

const res = await fetch('https://api.moddex.tv/v1/users/<login>/roles/<channel>');
const data = await res.json();
curl https://api.moddex.tv/v1/users/<login>/roles/<channel>
import requests
res = requests.get("https://api.moddex.tv/v1/users/<login>/roles/<channel>")
data = res.json()
GET /v1/channels/{login}/roles #

Every role this channel has granted — mods, vips and founders in one answer. For a single role, or to page it, use /v1/channels/{login}/{role}.

Parameters

NameInTypeDescription
login required path string The channel's twitch login.

Responses

CodeBodyDescription
200 ChannelRoles successful operation
404 no such channel
Response body
{
  "mods": [
    {
      "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,
      "bot": false,
      "chatBadge": null,
      "badges": [
        null
      ],
      "granted": "2023-01-01T00:00:00.000Z"
    }
  ],
  "vips": [
    {
      "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,
      "bot": false,
      "chatBadge": null,
      "badges": [
        null
      ],
      "granted": "2023-01-01T00:00:00.000Z"
    }
  ],
  "founders": [
    {
      "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,
      "bot": false,
      "chatBadge": null,
      "badges": [
        null
      ],
      "granted": "2023-01-01T00:00:00.000Z"
    }
  ]
}

Example request

const res = await fetch('https://api.moddex.tv/v1/channels/<login>/roles');
const data = await res.json();
curl https://api.moddex.tv/v1/channels/<login>/roles
import requests
res = requests.get("https://api.moddex.tv/v1/channels/<login>/roles")
data = res.json()
GET /v1/channels/{login}/{role} #

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

NameInTypeDescription
login required 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}.
role required 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.

Responses

CodeBodyDescription
200 RolePage successful operation
404 no such channel, or no such role
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,
      "bot": false,
      "chatBadge": null,
      "badges": [
        null
      ],
      "granted": "2023-01-01T00:00:00.000Z"
    }
  ],
  "limit": 100,
  "hasMore": true,
  "cursor": "string",
  "total": 97450
}

Example request

// query: limit, cursor, dir
const res = await fetch('https://api.moddex.tv/v1/channels/<login>/<role>');
const data = await res.json();
curl https://api.moddex.tv/v1/channels/<login>/<role>
# query: limit, cursor, dir
import requests
# query: limit, cursor, dir
res = requests.get("https://api.moddex.tv/v1/channels/<login>/<role>")
data = res.json()
GET /v1/users/{login}/{role} #

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

NameInTypeDescription
login required path string The account's twitch login. Always a login, never an id — ids go to /v1/users/by-id/{id}.
role required 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.
dir query string Sort direction.

Responses

CodeBodyDescription
200 RolePage successful operation
400 a search term shorter than three characters
404 no such account, or no such role
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,
      "bot": false,
      "chatBadge": null,
      "badges": [
        null
      ],
      "granted": "2023-01-01T00:00:00.000Z"
    }
  ],
  "limit": 100,
  "hasMore": true,
  "cursor": "string",
  "total": 97450
}

Example request

// query: limit, q, cursor, sort, dir
const res = await fetch('https://api.moddex.tv/v1/users/<login>/<role>');
const data = await res.json();
curl https://api.moddex.tv/v1/users/<login>/<role>
# query: limit, q, cursor, sort, dir
import requests
# query: limit, q, cursor, sort, dir
res = requests.get("https://api.moddex.tv/v1/users/<login>/<role>")
data = res.json()
POST /v1/channels/{login}/reindex auth #

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

NameInTypeDescription
login required path string the twitch login, never an id. Use the by-id twin for an id

Responses

CodeBodyDescription
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()
POST /v1/channels/by-id/{id}/reindex auth #

The by-id twin of the route above. Same token, same bucket, same replies. An id never changes, a login does.

Parameters

NameInTypeDescription
id required path string the numeric twitch id

Responses

CodeBodyDescription
200 object accepted, queued or already waiting
400 the id is not a numeric twitch id
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/by-id/<id>/reindex', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` }
});
const data = await res.json();
curl -X POST https://api.moddex.tv/v1/channels/by-id/<id>/reindex \
  -H "Authorization: Bearer $MODDEX_TOKEN"
import requests
res = requests.request("POST", "https://api.moddex.tv/v1/channels/by-id/<id>/reindex", headers={"Authorization": f"Bearer {token}"})
data = res.json()

users

One tracked account, from either side

GET /v1/users/{login} #

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

NameInTypeDescription
login required 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}.
include query string roles adds the first page of each role list.

Responses

CodeBodyDescription
200 User successful operation
404 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()
GET /v1/users/by-id/{id} #

The same account, addressed by its Twitch id.

Parameters

NameInTypeDescription
id required path string The numeric twitch id.
include query string As on the login form.

Responses

CodeBodyDescription
200 User successful operation
404 no such account
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/by-id/<id>');
const data = await res.json();
curl https://api.moddex.tv/v1/users/by-id/<id>
# query: include
import requests
# query: include
res = requests.get("https://api.moddex.tv/v1/users/by-id/<id>")
data = res.json()
GET /v1/channels/{login} #

The same Twitch account seen from the other side: what it has granted rather than what it holds.

Parameters

NameInTypeDescription
login required path string The channel's twitch login. Ids go to /v1/channels/by-id/{id}.

Responses

CodeBodyDescription
200 User successful operation
404 no such channel
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

const res = await fetch('https://api.moddex.tv/v1/channels/<login>');
const data = await res.json();
curl https://api.moddex.tv/v1/channels/<login>
import requests
res = requests.get("https://api.moddex.tv/v1/channels/<login>")
data = res.json()
GET /v1/channels/by-id/{id} #

The same channel, addressed by its Twitch id.

Parameters

NameInTypeDescription
id required path string The numeric twitch id.

Responses

CodeBodyDescription
200 User successful operation
404 no such channel
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

const res = await fetch('https://api.moddex.tv/v1/channels/by-id/<id>');
const data = await res.json();
curl https://api.moddex.tv/v1/channels/by-id/<id>
import requests
res = requests.get("https://api.moddex.tv/v1/channels/by-id/<id>")
data = res.json()

browse

Paged lists of indexed channels and accounts

GET /v1/channels #

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

NameInTypeDescription
sort query string read (default, most recently scraped first), roles, followers
limit query integer 1 to 100, default 25
offset query integer 0 to 10000. deep paging is capped on purpose

Responses

CodeBodyDescription
200 BrowsePage successful operation
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": null,
      "badges": [
        null
      ],
      "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": 25,
  "offset": 0,
  "hasMore": true
}

Example request

// query: sort, limit, offset
const res = await fetch('https://api.moddex.tv/v1/channels');
const data = await res.json();
curl https://api.moddex.tv/v1/channels
# query: sort, limit, offset
import requests
# query: sort, limit, offset
res = requests.get("https://api.moddex.tv/v1/channels")
data = res.json()
GET /v1/users #

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

NameInTypeDescription
sort query string roles (default), followers
bots query string exclude to drop curated bot accounts. included by default
limit query integer 1 to 100, default 25
offset query integer 0 to 10000

Responses

CodeBodyDescription
200 BrowsePage successful operation
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": null,
      "badges": [
        null
      ],
      "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": 25,
  "offset": 0,
  "hasMore": true
}

Example request

// query: sort, bots, limit, offset
const res = await fetch('https://api.moddex.tv/v1/users');
const data = await res.json();
curl https://api.moddex.tv/v1/users
# query: sort, bots, limit, offset
import requests
# query: sort, bots, limit, offset
res = requests.get("https://api.moddex.tv/v1/users")
data = res.json()

badges

Badge definitions and who wears them

GET /v1/badges #

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

NameInTypeDescription
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

CodeBodyDescription
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.
400 invalid input
404 badge not found
Response body
{
  "id": 3,
  "slug": "top-donator",
  "name": "top donator",
  "chatName": "moddex top donator",
  "order": 30,
  "wearable": true,
  "images": {
    "icon": {
      "svg": "https://moddex.tv/badges/top-donator/icon.svg",
      "png": "https://moddex.tv/badges/top-donator/icon.png"
    },
    "chat": {
      "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"
    }
  }
}

Example request

// query: slug, id, name
const res = await fetch('https://api.moddex.tv/v1/badges');
const data = await res.json();
curl https://api.moddex.tv/v1/badges
# query: slug, id, name
import requests
# query: slug, id, name
res = requests.get("https://api.moddex.tv/v1/badges")
data = res.json()
GET /v1/chat-badges #

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.

Responses

CodeBodyDescription
200 ChatBadgeGroup[] successful operation
Response body
[
  {
    "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"
    },
    "users": [
      "401235515"
    ]
  }
]

Example request

const res = await fetch('https://api.moddex.tv/v1/chat-badges');
const data = await res.json();
curl https://api.moddex.tv/v1/chat-badges
import requests
res = requests.get("https://api.moddex.tv/v1/chat-badges")
data = res.json()

stats

Aggregate counts

GET /v1/leaderboards/{role} #

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

NameInTypeDescription
role required 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.

Responses

CodeBodyDescription
200 Leaderboard successful operation
400 a malformed limit, after or bots value
Response body
{
  "role": "mod",
  "computedAt": "2026-08-26T04:00:00.000Z",
  "depth": 1000,
  "of": 6130351,
  "comparedTo": "2026-08-19",
  "items": [
    {
      "place": 1,
      "moved": 2,
      "count": 905753,
      "id": "19264788",
      "login": "nightbot",
      "name": "Nightbot",
      "avatar": "string",
      "bot": true,
      "badges": [
        null
      ]
    }
  ]
}

Example request

// query: limit, after, bots
const res = await fetch('https://api.moddex.tv/v1/leaderboards/<role>');
const data = await res.json();
curl https://api.moddex.tv/v1/leaderboards/<role>
# query: limit, after, bots
import requests
# query: limit, after, bots
res = requests.get("https://api.moddex.tv/v1/leaderboards/<role>")
data = res.json()
GET /v1/stats #

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.

Responses

CodeBodyDescription
200 Stats successful operation
Response body
{
  "channels": 1220000,
  "users": 8200000,
  "mods": 6100000,
  "vips": 900000,
  "founders": 0,
  "takenAt": "2026-08-15T03:00:00.000Z"
}

Example request

const res = await fetch('https://api.moddex.tv/v1/stats');
const data = await res.json();
curl https://api.moddex.tv/v1/stats
import requests
res = requests.get("https://api.moddex.tv/v1/stats")
data = res.json()
GET /v1/stats/history #

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.

Parameters

NameInTypeDescription
days query integer

Responses

CodeBodyDescription
200 StatsPoint[] successful operation
Response body
[
  {
    "day": "2026-08-15",
    "channels": 1220000,
    "users": 8200000,
    "mods": 6100000,
    "vips": 900000,
    "founders": 0
  }
]

Example request

// query: days
const res = await fetch('https://api.moddex.tv/v1/stats/history');
const data = await res.json();
curl https://api.moddex.tv/v1/stats/history
# query: days
import requests
# query: days
res = requests.get("https://api.moddex.tv/v1/stats/history")
data = res.json()

eventsub

Whether live role updates are being received

GET /v1/eventsub/health #

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.

Responses

CodeBodyDescription
200 EventsubHealth at least one shard is receiving, or eventsub is off
Response body
{
  "status": "ok",
  "service": "moddex-api",
  "eventsub": "receiving",
  "enabledShards": 1,
  "totalShards": 1
}

Example request

const res = await fetch('https://api.moddex.tv/v1/eventsub/health');
const data = await res.json();
curl https://api.moddex.tv/v1/eventsub/health
import requests
res = requests.get("https://api.moddex.tv/v1/eventsub/health")
data = res.json()

Schemas

Badge #
FieldTypeDescriptionExample
id required integer 3
slug required string The stable key. Match on this, never on name. top-donator
name required string Display text, and rewordable. Not a key. top donator
svg required string https://moddex.tv/badges/top-donator/icon.svg
IconUrls #
FieldTypeDescriptionExample
svg required string https://moddex.tv/badges/top-donator/icon.svg
png required string https://moddex.tv/badges/top-donator/icon.png
ChatUrls #
FieldTypeDescriptionExample
1 required string https://moddex.tv/badges/top-donator/1x.webp
2 required string https://moddex.tv/badges/top-donator/2x.webp
4 required string https://moddex.tv/badges/top-donator/4x.webp
BadgeImages #
FieldTypeDescriptionExample
icon required IconUrls
chat required ChatUrls
BadgeCatalogueEntry #
FieldTypeDescriptionExample
id required integer 3
slug required string top-donator
name required string top donator
chatName required string What a chat client renders, with the vendor prefix. Null when the badge cannot be worn. moddex top donator
order required integer Where it sits in a badge row, read left to right. 30
wearable required boolean Whether somebody may wear this in chat. Four of the nine may not, and they still carry every image. true
images required BadgeImages
ChatBadge #
FieldTypeDescriptionExample
slug required string top-donator
name required string moddex top donator
images required ChatUrls
ChatBadgeGroup #
FieldTypeDescriptionExample
slug required string top-donator
name required string moddex top donator
images required ChatUrls
users required string[] The ids of every account wearing this badge. 401235515
User #
FieldTypeDescriptionExample
id required string 217986157
login required string maersux
name required string Twitch display name. maersux
avatar required string https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png
follower integer 317
discord string 780910551286546493
bot boolean Set from the curated bot list. Bots dominate large mod lists. false
banned string Twitch's reason, or an empty string if not banned.
chatBadge ChatBadge | null
badges required Badge[]
bio required string The Twitch account bio. this random dev guy. if there's a social media platform with this name, it's probably me.
created required string When the Twitch account was created. 2018-05-01T18:09:47.000Z
updated string When this account was last read as a channel — its roles. It is not a freshness stamp for the fields around it. 2024-01-01T00:00:00.000Z
Role #
FieldTypeDescriptionExample
id required string 217986157
login required string maersux
name required string maersux
avatar required string https://static-cdn.jtvnw.net/jtv_user_pictures/4058d275-ca87-4cf3-b736-c0392b81b6ed-profile_image-300x300.png
follower required integer 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. 317
bot required boolean Set from the curated bot list. Bots dominate large mod lists. false
chatBadge ChatBadge | null Always null on this endpoint — role lists do not read chat badges.
badges required Badge[]
granted required string 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. 2023-01-01T00:00:00.000Z
RolePage #
FieldTypeDescriptionExample
items required Role[]
limit required integer 100
hasMore required boolean true
cursor required string | null 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 required integer | null The size of the whole list, on the FIRST page only. A null on a later page means unchanged, not unknown. 97450
RoleCounts #
FieldTypeDescriptionExample
mod required integer 12
vip required integer 3
founder required integer 0
total required integer 15
BrowsePage #
FieldTypeDescriptionExample
items required User[] Each entry is a User carrying one extra field, counts (RoleCounts).
limit required integer 25
offset required integer 0
hasMore required boolean Read one row past the page rather than counted, so it costs nothing. true
Stats #
FieldTypeDescriptionExample
channels required integer 1220000
users required integer 8200000
mods required integer 6100000
vips required integer 900000
founders required integer | null null means not counted at the time of this snapshot, not none.
takenAt required string | null When the snapshot was taken. null on an install that has not reached its first 03:00 UTC. 2026-08-15T03:00:00.000Z
StatsPoint #
FieldTypeDescriptionExample
day required string 2026-08-15
channels required integer 1220000
users required integer 8200000
mods required integer 6100000
vips required integer 900000
founders required integer | null null for days before founders were measured. Break the line, do not draw a zero.
TopDonator #
FieldTypeDescriptionExample
rank required integer 1
userId required string 217986157
login required string | null null while the donating account has never been fetched into the database. maersux
displayName required string | null maersux
totalCents required integer Cents, so no currency arithmetic happens in json. 2500
payments required integer 3
latest required string | null 2026-08-01T12:00:00.000Z
Grant #
FieldTypeDescriptionExample
grantedAt required string When twitch says it was granted. Null when twitch gave no date. 2025-02-09T17:22:48.000Z
Membership #
FieldTypeDescriptionExample
mod required Grant
vip required Grant
founder required Grant
ChannelRoles #
FieldTypeDescriptionExample
mods required Role[]
vips required Role[]
founders required Role[]
LeaderRow #
FieldTypeDescriptionExample
place required integer 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. 2
count required integer 905753
id required string 19264788
login required string nightbot
name required string Nightbot
avatar string
bot boolean true
badges Badge[]
Leaderboard #
FieldTypeDescriptionExample
role required string mod
computedAt required string Copied from the rollup, never defaulted, so both endpoints agree. 2026-08-26T04:00:00.000Z
depth required integer How deep the board is materialised. Past it there is nothing to serve. 1000
of required 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. 2026-08-19
items required LeaderRow[]
EventsubHealth #
FieldTypeDescriptionExample
status required string ok
service required string moddex-api
eventsub required string receiving
enabledShards integer 1
totalShards integer One shard, deliberately. More than one reports itself as a problem. 1
Error #
FieldTypeDescriptionExample
error required string the status text, always not found
status required integer http status code 404
message string optional detail, for humans no badge with id 9