For mod managers
This page is for authors of mod managers: how to add Glitchmug as a source. We don't make a manager of our own, because good ones already exist.
You can start without a key. The catalog is public, the same data anyone sees on the site, so a key isn't needed to read it or download files. Keys only lift limits.
The endpoints
| Step | Request |
|---|---|
| Games | GET /games, then GET /games/{slug}/categories |
| Browse and search | GET /mods?game=deadlock with q, category, tag, sort |
| What changed | GET /mods?game=deadlock&updatedSince=… |
| One mod | GET /mods/{id} |
| Versions and changelogs | GET /mods/{id}/versions, newest first |
| Files of a version | GET /versions/{id}/files: name, size, sha256 |
| What is inside an archive | GET /files/{id}/entries |
| Download | GET /files/{id}/download |
The full schema is in the API reference.
Checking for updates
Store id and currentVersion of each installed mod. On a sync, ask for what changed since the last one and compare versions: updatedAt also moves when counters change, so a newer date alone does not mean a new release.
- curl
- TypeScript
- Python
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${GLITCHMUG_API:-https://api.glitchmug.com/v1}"
UA="ExampleManager/1.0 (+https://example.com)"
# Everything changed since your last sync; compare currentVersion with what you stored
curl -fsS -A "$UA" "$BASE_URL/mods?game=deadlock&updatedSince=2026-10-01T00:00:00Z&sort=updated&limit=100"
const baseUrl = process.env.GLITCHMUG_API ?? 'https://api.glitchmug.com/v1';
const headers = { 'User-Agent': 'ExampleManager/1.0 (+https://example.com)' };
type Page = {
data: { id: number; title: string; currentVersion: string | null; updatedAt: string }[];
page: { nextCursor: string | null; hasMore: boolean };
};
// What the manager stored on the last sync: mod id -> version
const known = new Map<number, string>();
const since = '2026-10-01T00:00:00Z';
// Walk the pages of what changed (stop after 3 here); updatedAt also moves on counters, so compare versions
let cursor: string | null = null;
for (let i = 0; i < 3; i++) {
const q = new URLSearchParams({ game: 'deadlock', updatedSince: since, sort: 'updated', limit: '100' });
if (cursor) q.set('cursor', cursor);
const res = await fetch(`${baseUrl}/mods?${q}`, { headers });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const body = (await res.json()) as Page;
for (const m of body.data) {
if (m.currentVersion && known.get(m.id) !== m.currentVersion) console.log('update:', m.id, m.title, m.currentVersion);
}
if (!body.page.hasMore) break;
cursor = body.page.nextCursor;
}
import json
import os
import urllib.parse
import urllib.request
base_url = os.environ.get("GLITCHMUG_API", "https://api.glitchmug.com/v1")
headers = {"User-Agent": "ExampleManager/1.0 (+https://example.com)"}
# What the manager stored on the last sync: mod id -> version
known = {}
since = "2026-10-01T00:00:00Z"
# Walk the pages of what changed (stop after 3 here); updatedAt also moves on counters, so compare versions
cursor = None
for _ in range(3):
query = {"game": "deadlock", "updatedSince": since, "sort": "updated", "limit": 100}
if cursor:
query["cursor"] = cursor
req = urllib.request.Request(f"{base_url}/mods?{urllib.parse.urlencode(query)}", headers=headers)
with urllib.request.urlopen(req) as res:
body = json.load(res)
for mod in body["data"]:
if mod["currentVersion"] and known.get(mod["id"]) != mod["currentVersion"]:
print("update:", mod["id"], mod["title"], mod["currentVersion"])
if not body["page"]["hasMore"]:
break
cursor = body["page"]["nextCursor"]
A mod that is no longer available answers 404. isWip marks an unfinished mod, and GET /mods/{id} also has isObsolete for an outdated one. 18+ mods are left out of lists unless you pass nsfw=include, and isNsfw marks them: keep them behind your own age check.
Webhooks only report changes to your own mods, so a manager polls. Once in a few minutes is plenty.
Downloading
GET /files/{id}/download counts the download for the author and answers 302 to a signed link on https://dl.glitchmug.com. Follow the redirect and check the file against sha256.
- curl
- TypeScript
- Python
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${GLITCHMUG_API:-https://api.glitchmug.com/v1}"
UA="ExampleManager/1.0 (+https://example.com)"
VERSION_ID="${VERSION_ID:-twl7516oswn1}"
FILE_ID="${FILE_ID:-5ann2e2oz3y6}"
# The files of a version, with name, size and sha256
curl -fsS -A "$UA" "$BASE_URL/versions/$VERSION_ID/files"
# -L follows the 302 to the signed link on dl.glitchmug.com
out="$(mktemp)"
curl -fsSL -A "$UA" -o "$out" "$BASE_URL/files/$FILE_ID/download"
# Compare with the sha256 from the list above; on a mismatch discard the file
sha256sum "$out"
rm -f "$out"
import { createHash } from 'node:crypto';
const baseUrl = process.env.GLITCHMUG_API ?? 'https://api.glitchmug.com/v1';
const headers = { 'User-Agent': 'ExampleManager/1.0 (+https://example.com)' };
const versionId = process.env.VERSION_ID ?? 'twl7516oswn1';
type Files = { data: { id: string; name: string; size: number; sha256: string; scanStatus: string }[] };
const list = await fetch(`${baseUrl}/versions/${versionId}/files`, { headers });
if (!list.ok) throw new Error(`HTTP ${list.status}: ${await list.text()}`);
const { data } = (await list.json()) as Files;
for (const file of data) {
// fetch follows the 302 to the signed link on dl.glitchmug.com
const res = await fetch(`${baseUrl}/files/${file.id}/download`, { headers });
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const bytes = Buffer.from(await res.arrayBuffer());
const hash = createHash('sha256').update(bytes).digest('hex');
console.log(file.name, hash === file.sha256 ? 'ok' : 'sha256 mismatch: discard the file');
}
import hashlib
import json
import os
import urllib.request
base_url = os.environ.get("GLITCHMUG_API", "https://api.glitchmug.com/v1")
headers = {"User-Agent": "ExampleManager/1.0 (+https://example.com)"}
version_id = os.environ.get("VERSION_ID", "twl7516oswn1")
def get(path):
return urllib.request.urlopen(urllib.request.Request(f"{base_url}{path}", headers=headers))
with get(f"/versions/{version_id}/files") as res:
files = json.load(res)["data"]
for file in files:
# urllib follows the 302 to the signed link on dl.glitchmug.com
with get(f"/files/{file['id']}/download") as res:
digest = hashlib.sha256(res.read()).hexdigest()
print(file["name"], "ok" if digest == file["sha256"] else "sha256 mismatch: discard the file")
- Always start from this endpoint. The signed link lives 6 hours: do not store it, and do not host copies of the files, or authors lose their download counts and the ability to remove their work.
- Only files that passed the virus scan are listed.
- Without a key downloads are limited to 2 MB/s. A client key lifts that.
Limits and keys
There are three ways to call the API:
| No key | Client key | API key | |
|---|---|---|---|
| For | Trying things out, an app with few downloads | Your app, on your users' computers | Your own backend |
| Requests | 60 / min per IP | 60 / min per IP | 600 / min per key |
| Download speed | 2 MB/s | No limit | No limit |
| How to send it | Nothing to send | X-Glitchmug-Client: <key> on downloads | Authorization: Bearer <key> |
| How to get it | Free, write to contact@glitchmug.com | Settings → API keys |
- A client key only lifts the speed limit, so it may ship in your app.
- An API key acts as your account and can change your mods, so never ship it in an app build.
- 60 requests a minute per IP is enough for a desktop app that syncs with
updatedSincerather than paging through the catalog. See Rate limits. - The API sends no CORS headers, so call it from native code or your backend, not from a page in a webview. In Tauri or Electron that means the native side, not the frontend.
Being a good client
- Send a
User-Agentwith your app's name, version and a link, as in the examples: it lets us reach you before a problem turns into a block. - Show the mod's author, and link to its page:
pathin every answer, onhttps://glitchmug.com. For Russian pages put/ruin front. - Show the
licensefromGET /mods/{id}. It says what an author allows, including re-uploads. - Honour
Retry-Afteron429.
Stability
Additions ship in /v1. A breaking change would ship as /v2, with v1 kept running during a migration window. Every change is in the Changelog. Until the public beta v1 may still change, and such changes are marked Breaking.
Working together
Write to contact@glitchmug.com if you maintain a manager. We'll send a client key and can talk about:
- an "Install with …" button on mod pages, if your manager has a URL protocol;
- what the API is missing for you.