Вебхуки
Вместо постоянных опросов Glitchmug сам отправит POST на ваш сервер, когда что-то произойдёт с вашими модами или жалобами.
Настройка
В Настройках → Вебхуки на сайте или ключом со scope webhooks:manage:
| Запрос | Что делает |
|---|---|
POST /me/webhooks | Добавить: { "url": "https://…", "events": ["version.published"] }. В ответе — секрет подписи secret, показывается один раз |
GET /me/webhooks | Список ваших вебхуков с последней отправкой каждого |
PATCH /me/webhooks/{id} | Изменить url, events или isActive |
POST /me/webhooks/{id}/rotate | Новый секрет; старый перестаёт работать сразу |
POST /me/webhooks/{id}/ping | Отправить событие ping прямо сейчас |
GET /me/webhooks/{id}/deliveries | Последние 50 попыток (хранятся 14 дней) |
DELETE /me/webhooks/{id} | Удалить |
До 10 вебхуков на аккаунт. Адрес должен быть https:// и вести в публичный интернет: приватные, loopback и link-local адреса запрещены, в том числе когда публичное имя указывает на такой адрес. Редиректы не выполняются.
События
| Событие | Когда | data |
|---|---|---|
mod.updated | Что-то изменилось в вашем моде: правка, статус, модерация | mod |
version.published | Версия вашего мода стала публичной | mod, version |
comment.created | Кто-то прокомментировал ваш мод | mod, comment |
report.resolved | Ваша жалоба закрыта (решена или отклонена) | report |
review.created | Кто-то оставил отзыв на ваш мод | mod, review (id, recommend, body, author, createdAt) |
ping | Вы запросили проверку | webhook |
mod.updated склеивается: несколько изменений за пару секунд приходят одним событием с последним состоянием.
Отправка
POST /your/endpoint
Content-Type: application/json
User-Agent: glitchmug-webhooks/1
Glitchmug-Event: version.published
Glitchmug-Delivery: 8c1f4a2e-6b7d-4f0e-9a51-2d3c4b5a6e7f
Glitchmug-Signature: t=1792843200,v1=5f2b…
{
"id": "8c1f4a2e-6b7d-4f0e-9a51-2d3c4b5a6e7f",
"event": "version.published",
"createdAt": "2026-10-26T12:00:00.000Z",
"data": {
"mod": {
"id": 1042,
"slug": "neon-haze-skin",
"title": "Neon Haze skin",
"status": "published",
"game": "deadlock",
"url": "https://glitchmug.com/mods/1042/neon-haze-skin",
"updatedAt": "2026-10-26T11:59:58.000Z"
},
"version": { "id": "v1v2v3v4v5v6", "version": "1.2.0", "publishedAt": "2026-10-26T11:59:58.000Z" }
}
}
Ответьте любым 2xx в течение 10 секунд. Тяжёлую работу делайте потом: положите событие в очередь и сразу ответьте.
Проверка подписи
Glitchmug-Signature имеет вид t=<unix-секунды>,v1=<hex>, где v1 = HMAC-SHA256(secret, "<t>.<сырое тело>").
- Возьмите сырые байты тела запроса, до разбора JSON.
- Отклоните запрос, если
tотличается от текущего времени больше чем на 5 минут (защита от повторов). - Посчитайте HMAC и сравните с
v1за постоянное время.
- TypeScript
- Python
import { createHmac, timingSafeEqual } from 'node:crypto';
/** Check `Glitchmug-Signature` against the raw request body (not re-serialized JSON). */
export function verify(secret: string, rawBody: string, header: string, now = Date.now()): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(now / 1000 - t) > 300) return false; // 5 min replay window
const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
const given = Buffer.from(parts.v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Self-check with a sample delivery
const secret = 'whsec_example';
const body = '{"id":"6f1c…","event":"ping","createdAt":"2026-10-26T12:00:00.000Z","data":{}}';
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac('sha256', secret).update(`${t}.${body}`).digest('hex');
if (!verify(secret, body, `t=${t},v1=${v1}`)) throw new Error('valid signature rejected');
if (verify(secret, body + ' ', `t=${t},v1=${v1}`)) throw new Error('tampered body accepted');
console.log('signature check ok');
import hashlib
import hmac
import time
def verify(secret: str, raw_body: bytes, header: str, now: float | None = None) -> bool:
"""Check Glitchmug-Signature against the raw request body (not re-serialized JSON)."""
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
given = parts["v1"]
except (KeyError, ValueError):
return False
if abs((now or time.time()) - t) > 300: # 5 min replay window
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(given, expected)
# Self-check with a sample delivery
secret = "whsec_example"
body = b'{"id":"6f1c...","event":"ping","createdAt":"2026-10-26T12:00:00.000Z","data":{}}'
t = int(time.time())
v1 = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
assert verify(secret, body, f"t={t},v1={v1}"), "valid signature rejected"
assert not verify(secret, body + b" ", f"t={t},v1={v1}"), "tampered body accepted"
print("signature check ok")
Повторы и отключение
- Ответ не
2xx, таймаут или ошибка соединения — 6 повторов: через 1 минуту, 5 минут, 30 минут, 2 часа, 6 часов и 12 часов — около 21 часа от первой попытки до последней. Короткий простой вашего сервера ничего не теряет. - У всех попыток одного события одинаковые
Glitchmug-Deliveryи тело — используйте его, чтобы отбрасывать дубли. - После 10 событий подряд, которые не доставились ни с одной попытки, вебхук выключается (
isActive: false). Включите его снова черезPATCH { "isActive": true }— счётчик ошибок обнулится. - Порядок доставки не гарантирован. Если он важен, ориентируйтесь на
updatedAt/createdAtв теле.