REST API v1 · Разработчикам
Вебхуки: события Discord-сервера на вашем сервере
Вместо того чтобы опрашивать API, пусть BotGG сам вызывает вас: когда на Discord-сервере что-то происходит — меняется баланс, участник получает уровень, совершается покупка, открывается тикет, — BotGG отправляет подписанный JSON-запрос POST на ваш HTTPS-эндпоинт.
Документация для разработчиков
На этой странице
Настройка эндпоинта
- В консоли откройте раздел API своего сервера и добавьте эндпоинт вебхука: URL с
https://и нужные события. - Скопируйте секрет подписи (
whsec_…) — он показывается только один раз — и сохраните его на своём сервере, например вBOTGG_WEBHOOK_SECRET. - Отправьте тестовое событие из консоли: оно приходит с типом
webhook.test. Убедитесь, что эндпоинт проверяет подпись и отвечает2xx.
- Эндпоинтов на сервер: 1 на Free, 10 на Pro.
- URL должен быть публичным HTTPS-адресом без логина и пароля; приватные и внутренние адреса не принимаются.
- Смена секрета действует сразу — разверните новый секрет на своём сервере сразу после смены.
Как выглядит доставка
Каждая доставка — это POST с JSON-телом { id, type, createdAt, guildId, data } (data зависит от типа события, см. события) и такими заголовками:
| Заголовок | Описание |
|---|---|
BotGG-Webhook-Id | ID события (`evt_…`), одинаковый при всех повторах — сохраняйте его, чтобы отбрасывать дубликаты. |
BotGG-Webhook-Timestamp | Unix-время в секундах, когда подписана эта попытка. |
BotGG-Signature | Подпись HMAC-SHA256 от времени и «сырого» тела, сделанная секретом подписи вашего эндпоинта. |
POST /botgg-webhook HTTP/1.1
Content-Type: application/json
User-Agent: BotGG-Webhooks/1.0 (+https://botgg.app/developers/)
BotGG-Webhook-Id: evt_3Kq9Zx2mV7pL4nR8tW1y
BotGG-Webhook-Timestamp: 1791374460
BotGG-Signature: v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{"id":"evt_3Kq9Zx2mV7pL4nR8tW1y","type":"levels.levelUp","createdAt":"2026-10-07T12:00:00.000Z","guildId":"123456789012345678","data":{"userId":"876543210987654321","level":5,"previousLevel":4,"source":"activity"}}- Ответьте любым статусом
2xxв течение 10 секунд. Всё остальное — статус ошибки, тайм-аут или редирект (по редиректам BotGG не переходит) — считается неудачной попыткой. - Неудачные доставки повторяются через 1 минуту, 5 минут, 30 минут, 2 часа, 6 часов и 24 часа — всего до 7 попыток — с тем же
idсобытия. - Доставка может прийти несколько раз и не по порядку: отбрасывайте дубликаты по
id, а порядок определяйте поcreatedAt. - Эндпоинт, который долго не отвечает (сутки и дольше), автоматически отключается. Исправьте проблему и включите его снова в консоли — там же можно повторить неудачные доставки.
Проверка подписи
Всегда проверяйте, что запрос действительно пришёл от BotGG. Подпись — это HMAC-SHA256 в hex от заголовка времени, точки и «сырого» тела запроса, сделанная секретом вашего эндпоинта: BotGG-Signature: v1=<hex HMAC-SHA256(secret, "<timestamp>.<body>")>.
- Прочитайте тело ровно в том виде, в каком оно пришло (до разбора JSON).
- Отклоните запрос, если
BotGG-Webhook-Timestampотличается от ваших часов больше чем на 5 минут, — это защищает от повторной отправки. - Вычислите HMAC от
<timestamp>.<сырое тело>своим секретом и сравните его за постоянное время с каждым значениемv1=вBotGG-Signature.
// Node.js 18+ with Express. Verify against the RAW body — not a re-serialized JSON object.
import crypto from 'node:crypto';
import express from 'express';
const SECRET = process.env.BOTGG_WEBHOOK_SECRET; // whsec_… (shown once in the console)
const TOLERANCE_SECONDS = 300;
function verify(rawBody, timestamp, signatureHeader) {
const ts = Number(timestamp);
if (!Number.isInteger(ts) || Math.abs(Date.now() / 1000 - ts) > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${rawBody}`).digest('hex');
return String(signatureHeader ?? '')
.split(/[\s,]+/)
.some((part) => {
const m = /^v1=([0-9a-f]{64})$/.exec(part);
return Boolean(m) && crypto.timingSafeEqual(Buffer.from(m[1]), Buffer.from(expected));
});
}
const app = express();
app.post('/botgg-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verify(raw, req.get('BotGG-Webhook-Timestamp'), req.get('BotGG-Signature'))) return res.status(400).end();
const event = JSON.parse(raw);
res.status(204).end(); // acknowledge within 10 s, then do the work
// event.id is the same on every retry: skip events you have already handled
console.log(event.type, event.data);
});
app.listen(3000);# Python 3 with Flask. Verify against the RAW body bytes.
import hashlib, hmac, json, os, time
from flask import Flask, abort, request
SECRET = os.environ["BOTGG_WEBHOOK_SECRET"].encode() # whsec_… (shown once in the console)
TOLERANCE_SECONDS = 300
def verify(raw: bytes, timestamp, header) -> bool:
try:
ts = int(timestamp)
except (TypeError, ValueError):
return False
if abs(time.time() - ts) > TOLERANCE_SECONDS:
return False
expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
for part in (header or "").replace(",", " ").split():
if part.startswith("v1=") and hmac.compare_digest(part[3:], expected):
return True
return False
app = Flask(__name__)
@app.post("/botgg-webhook")
def botgg_webhook():
raw = request.get_data() # the exact bytes BotGG signed
if not verify(raw, request.headers.get("BotGG-Webhook-Timestamp"), request.headers.get("BotGG-Signature")):
abort(400)
event = json.loads(raw)
# event["id"] is the same on every retry: skip events you have already handled
print(event["type"], event["data"])
return "", 204События
Подписывайте каждый эндпоинт только на нужные события. Форма данных в v1 стабильна: поля могут добавляться, но не удаляются и не меняют тип — просто игнорируйте незнакомые поля. ID Discord — строки, ID записей BotGG (операций, покупок, тикетов…) — целые числа.
| Событие | Модуль | Описание |
|---|---|---|
bank.transaction | Банк | Операция Банка — Любое изменение баланса: ежедневные награды, переводы, покупки, изменения персонала и через API. |
levels.levelUp | Уровни | Новый уровень — Участник достиг более высокого уровня (за активность или по решению персонала). |
shop.purchase | Магазин | Покупка — Участник купил товар в Магазине. |
shop.refund | Магазин | Возврат — Покупка в Магазине возвращена. |
tickets.opened | Тикеты | Тикет открыт — Создан канал или ветка тикета. |
tickets.closed | Тикеты | Тикет закрыт — Тикет закрыт (вручную, автоматически или после ухода автора). |
members.joined | Участники | Участник пришёл — Кто-то присоединился к серверу. |
members.left | Участники | Участник ушёл — Кто-то покинул сервер (или был исключён / забанен). |
giveaways.ended | Розыгрыши | Розыгрыш завершён — Розыгрыш завершён, победители выбраны. |
moderation.caseCreated | Модерация | Случай модерации — Записан новый случай модерации (предупреждение, тайм-аут, кик, бан, …). |
applications.submitted | Заявки | Заявка отправлена — Участник отправил форму заявки (с ответами). |
appeals.submitted | Апелляции | Апелляция отправлена — Забаненный пользователь подал апелляцию (с ответами). |
bank.transaction Операция Банка
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
transactionId | integer | Да | не меньше 0 |
userId | string | Да | Discord snowflake id Discord ID (строка) |
type | string | Да | Ledger type: daily, transfer_in, transfer_out, fee, admin_add, admin_remove, admin_set, flow, api, … |
amount | integer | Да | Signed change (negative = debit) |
balanceAfter | integer | Да | |
reason | string | null | Да | |
counterpartyId | string | null | Да | Discord snowflake id Discord ID (строка) |
actorId | string | null | Да | Discord snowflake id Discord ID (строка) |
createdAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "bank.transaction",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"transactionId": 98765,
"userId": "876543210987654321",
"type": "example",
"amount": 100,
"balanceAfter": 1350,
"reason": "Tournament prize",
"counterpartyId": "234567890123456789",
"actorId": "234567890123456789",
"createdAt": "2026-10-07T12:00:00.000Z"
}
}levels.levelUp Новый уровень
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
userId | string | Да | Discord snowflake id Discord ID (строка) |
level | integer | Да | не меньше 0 |
previousLevel | integer | Да | не меньше 0 |
source | string | Да | Одно из activity, admin |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "levels.levelUp",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"userId": "876543210987654321",
"level": 5,
"previousLevel": 4,
"source": "activity"
}
}shop.purchase Покупка
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
purchaseId | integer | Да | не меньше 0 |
userId | string | Да | Discord snowflake id Discord ID (строка) |
item | object | Да | |
item.id | integer | null | Да | null when the item was deleted since не меньше 0 |
item.name | string | Да | |
item.emoji | string | null | Да | |
item.type | string | Да | role | color | item | … |
store | object | Да | id/name null = the built-in Main store |
store.id | integer | null | Да | не меньше 0 |
store.name | string | null | Да | |
price | integer | Да | не меньше 0 |
status | string | Да | owned | used | refunded |
roleId | string | null | Да | Discord snowflake id Discord ID (строка) |
expiresAt | string | null | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
createdAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "shop.purchase",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"purchaseId": 98765,
"userId": "876543210987654321",
"item": {
"id": 98765,
"name": "Example",
"emoji": "🪙",
"type": "example"
},
"store": {
"id": 98765,
"name": "Example"
},
"price": 250,
"status": "example",
"roleId": "234567890123456789",
"expiresAt": "2026-10-07T12:00:00.000Z",
"createdAt": "2026-10-07T12:00:00.000Z"
}
}shop.refund Возврат
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
purchaseId | integer | Да | не меньше 0 |
userId | string | Да | Discord snowflake id Discord ID (строка) |
item | object | Да | |
item.id | integer | null | Да | null when the item was deleted since не меньше 0 |
item.name | string | Да | |
item.emoji | string | null | Да | |
item.type | string | Да | role | color | item | … |
price | integer | Да | не меньше 0 |
refundedBy | string | null | Да | Discord snowflake id Discord ID (строка) |
refundedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "shop.refund",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"purchaseId": 98765,
"userId": "876543210987654321",
"item": {
"id": 98765,
"name": "Example",
"emoji": "🪙",
"type": "example"
},
"price": 250,
"refundedBy": "234567890123456789",
"refundedAt": "2026-10-07T12:00:00.000Z"
}
}tickets.opened Тикет открыт
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
ticketId | integer | Да | не меньше 0 |
number | integer | Да | |
userId | string | Да | Discord snowflake id Discord ID (строка) |
panelId | integer | null | Да | не меньше 0 |
channelId | string | null | Да | Discord snowflake id Discord ID (строка) |
subject | string | null | Да | |
openedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "tickets.opened",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"ticketId": 98765,
"number": 7,
"userId": "876543210987654321",
"panelId": 98765,
"channelId": "234567890123456789",
"subject": "Help with my order",
"openedAt": "2026-10-07T12:00:00.000Z"
}
}tickets.closed Тикет закрыт
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
ticketId | integer | Да | не меньше 0 |
number | integer | Да | |
userId | string | Да | Discord snowflake id Discord ID (строка) |
panelId | integer | null | Да | не меньше 0 |
channelId | string | null | Да | Discord snowflake id Discord ID (строка) |
closedBy | string | null | Да | Discord snowflake id Discord ID (строка) |
closeReason | string | null | Да | |
closeType | string | null | Да | manual | auto | leave | … |
openedAt | string | null | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
closedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "tickets.closed",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"ticketId": 98765,
"number": 7,
"userId": "876543210987654321",
"panelId": 98765,
"channelId": "234567890123456789",
"closedBy": "234567890123456789",
"closeReason": "example",
"closeType": "example",
"openedAt": "2026-10-07T12:00:00.000Z",
"closedAt": "2026-10-07T12:00:00.000Z"
}
}members.joined Участник пришёл
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
userId | string | Да | Discord snowflake id Discord ID (строка) |
bot | boolean | Да | |
joinedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
accountCreatedAt | string | null | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "members.joined",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"userId": "876543210987654321",
"bot": false,
"joinedAt": "2026-10-07T12:00:00.000Z",
"accountCreatedAt": "2026-10-07T12:00:00.000Z"
}
}members.left Участник ушёл
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
userId | string | Да | Discord snowflake id Discord ID (строка) |
bot | boolean | Да | |
leftAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "members.left",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"userId": "876543210987654321",
"bot": false,
"leftAt": "2026-10-07T12:00:00.000Z"
}
}giveaways.ended Розыгрыш завершён
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
giveawayId | integer | Да | не меньше 0 |
prize | string | Да | |
channelId | string | null | Да | Discord snowflake id Discord ID (строка) |
messageId | string | null | Да | Discord snowflake id Discord ID (строка) |
hostId | string | null | Да | Discord snowflake id Discord ID (строка) |
winnerIds | array of string | Да | |
entrants | integer | Да | не меньше 0 |
endedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "giveaways.ended",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"giveawayId": 98765,
"prize": "Nitro",
"channelId": "234567890123456789",
"messageId": "234567890123456789",
"hostId": "234567890123456789",
"winnerIds": [
"234567890123456789"
],
"entrants": 42,
"endedAt": "2026-10-07T12:00:00.000Z"
}
}moderation.caseCreated Случай модерации
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
caseId | integer | Да | не меньше 0 |
number | integer | Да | |
action | string | Да | warn | timeout | kick | ban | tempban | unban | … |
targetId | string | Да | Discord snowflake id Discord ID (строка) |
moderatorId | string | null | Да | Discord snowflake id Discord ID (строка) |
reason | string | null | Да | |
durationSeconds | integer | null | Да | не меньше 0 |
source | string | null | Да | |
expiresAt | string | null | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
createdAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "moderation.caseCreated",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"caseId": 98765,
"number": 7,
"action": "example",
"targetId": "234567890123456789",
"moderatorId": "234567890123456789",
"reason": "Tournament prize",
"durationSeconds": 3600,
"source": "example",
"expiresAt": "2026-10-07T12:00:00.000Z",
"createdAt": "2026-10-07T12:00:00.000Z"
}
}applications.submitted Заявка отправлена
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
submissionId | integer | Да | не меньше 0 |
number | integer | Да | |
userId | string | Да | Discord snowflake id Discord ID (строка) |
formId | integer | null | Да | не меньше 0 |
formTitle | string | null | Да | |
status | string | Да | |
answers | array of object | Да | |
answers[].questionId | string | Да | |
answers[].label | string | Да | |
answers[].type | string | Да | Question type, e.g. short, paragraph, select, number, yesno |
answers[].value | string | null | Да | |
answers[].text | string | null | Да | Human-readable answer (as shown in Discord), or null |
submittedAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "applications.submitted",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"submissionId": 98765,
"number": 7,
"userId": "876543210987654321",
"formId": 98765,
"formTitle": "example",
"status": "example",
"answers": [
{
"questionId": "234567890123456789",
"label": "Example",
"type": "example",
"value": "example",
"text": "Example answer"
}
],
"submittedAt": "2026-10-07T12:00:00.000Z"
}
}appeals.submitted Апелляция отправлена
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
appealId | integer | Да | не меньше 0 |
number | integer | Да | |
userId | string | Да | Discord snowflake id Discord ID (строка) |
banCaseNumber | integer | null | Да | |
banReason | string | null | Да | |
answers | array of object | Да | |
answers[].questionId | string | Да | |
answers[].label | string | Да | |
answers[].type | string | Да | Question type, e.g. short, paragraph, select, number, yesno |
answers[].value | string | null | Да | |
answers[].text | string | null | Да | Human-readable answer (as shown in Discord), or null |
createdAt | string | Да | ISO-8601 timestamp (UTC) Дата и время ISO 8601 (UTC) |
{
"id": "evt_3Kq9Zx2mV7pL4nR8tW1y",
"type": "appeals.submitted",
"createdAt": "2026-10-07T12:00:00.000Z",
"guildId": "123456789012345678",
"data": {
"appealId": 98765,
"number": 7,
"userId": "876543210987654321",
"banCaseNumber": 10,
"banReason": "example",
"answers": [
{
"questionId": "234567890123456789",
"label": "Example",
"type": "example",
"value": "example",
"text": "Example answer"
}
],
"createdAt": "2026-10-07T12:00:00.000Z"
}
}