Uploading a mod
The same steps the website's upload form takes. Your key needs mods:write and files:upload.
draft mod ─► details, license, AI usage ─► version ─► upload files (tus) ─► release version ─► scan ─► submit mod
1. Create a draft
POST /mods with game, category and title (slugs come from GET /games and GET /games/{slug}/categories). Send an Idempotency-Key so a retry after a timeout doesn't create a second draft.
- curl
- TypeScript
- Python
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${GLITCHMUG_API:-https://api.glitchmug.com/v1}"
# Needs scope mods:write. Retrying with the same Idempotency-Key returns the same draft.
curl -fsS -X POST "$BASE_URL/mods" \
-H "Authorization: Bearer $GLITCHMUG_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid 2>/dev/null || echo "create-$(date +%s)")" \
-d '{"game": "deadlock", "category": "skins", "title": "Neon Haze skin"}'
const baseUrl = process.env.GLITCHMUG_API ?? 'https://api.glitchmug.com/v1';
// Needs scope mods:write. Retrying with the same Idempotency-Key returns the same draft.
const res = await fetch(`${baseUrl}/mods`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GLITCHMUG_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({ game: 'deadlock', category: 'skins', title: 'Neon Haze skin' }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
const mod = await res.json();
console.log('draft', mod.id, mod.status);
import json
import os
import urllib.request
import uuid
base_url = os.environ.get("GLITCHMUG_API", "https://api.glitchmug.com/v1")
# Needs scope mods:write. Retrying with the same Idempotency-Key returns the same draft.
req = urllib.request.Request(
f"{base_url}/mods",
method="POST",
data=json.dumps({"game": "deadlock", "category": "skins", "title": "Neon Haze skin"}).encode(),
headers={
"Authorization": f"Bearer {os.environ['GLITCHMUG_TOKEN']}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
)
with urllib.request.urlopen(req) as res:
mod = json.load(res)
print("draft", mod["id"], mod["status"])
The response holds the mod id you use below. Drafts are visible only to you.
2. Fill in the details
PATCH /mods/{id}—summary(required to submit),description(Markdown),tags,isNsfw,contentFlags,isWip…PUT /mods/{id}/license— the permissions checklist (download, redistribute, modify, reuse, commercial…), all items at once.PUT /mods/{id}/ai-usage— whether AI was used for art, code, audio, text and concept.
Both PUTs are required before you can submit.
3. Create a version
POST /mods/{id}/versions with version (e.g. 1.0.0) and an optional changelog. Supports Idempotency-Key too.
4. Upload files
Uploads speak tus 1.0 (core, creation, termination, expiration), so any tus client works — tus-js-client, tus-py-client — with the endpoint https://api.glitchmug.com/v1/versions/{versionId}/uploads and your Authorization header.
By hand:
POST /versions/{versionId}/uploads
Tus-Resumable: 1.0.0
Upload-Length: 52428800
Upload-Metadata: filename ZHJhZ29uLXNraW4uemlw
→ 201 Created
Location: /api/v1/uploads/{uploadId}
PATCH /uploads/{uploadId}
Tus-Resumable: 1.0.0
Upload-Offset: 0
Content-Type: application/offset+octet-stream
<up to 16 MB of bytes>
→ 204 No Content
Upload-Offset: 16777216
- Send chunks of at most 16 MB until
Upload-OffsetequalsUpload-Length. - Lost the connection?
HEAD /uploads/{uploadId}returns the offset to resume from. DELETE /uploads/{uploadId}aborts. Unfinished uploads expire.GET /uploads/{uploadId}(JSON, not tus) gives thefileIdonce complete.
Limits: files up to 500 MB (2 GB for trusted authors), storage quota 10 GB (50 GB trusted), 10 upload sessions per hour. Over the limit → 413 file_too_large / quota_exceeded, 429.
5. Release the version
POST /versions/{versionId}/submit. The version goes to processing while every file is checked: antivirus scan, archive listing, unpacked size. Then it is:
published— when the mod is already public and the game has no premoderation (or you may publish directly);pending— waiting for a moderator;- back to
draft— if no file passed, or a check could not finish. Refused files (infected,rejected) are never served; the version is released with the clean ones.
Watch it with GET /versions/{versionId}/files: each file has scanStatus (pending, scanning, clean, infected, rejected, failed) and, when refused, scanDetail (e.g. unpack_too_large). Or subscribe to the version.published webhook.
6. Add images
POST /images as multipart/form-data: file (jpeg, png, webp or gif, up to 10 MB), ownerType=mod, ownerId={modId}, kind=cover or screenshot, optional caption and sort. The answer is 202 with status pending; the image is ready a few seconds later. Up to 20 screenshots per mod; a new cover replaces the old one.
7. Submit the mod
POST /mods/{id}/submit once the mod has a summary, license, AI usage and a released version with clean files. It becomes published or pending (premoderation), like versions. A rejection comes with the rule it broke; fix the mod and submit again.
Later updates: create a new version (step 3–5). The mod stays public; its current version switches when the new one is published.