REST API v1 · למפתחים
Webhooks: אירועי שרת הדיסקורד ישר לשרת שלכם
במקום לתשאל את ה-API שוב ושוב, תנו ל-BotGG לפנות אליכם: כשמשהו קורה בשרת הדיסקורד — שינוי יתרה, עלייה ברמה, רכישה, כרטיס חדש — BotGG שולח בקשת POST חתומה עם JSON לנקודת הקצה שלכם ב-HTTPS.
תיעוד למפתחים
בעמוד הזה
הגדרת נקודת קצה
- בקונסולה, פתחו את אזור ה-API של השרת והוסיפו נקודת קצה ל-Webhook: כתובת
https://והאירועים שאתם רוצים. - העתיקו את סוד החתימה (
whsec_…) — הוא מוצג פעם אחת בלבד — ושמרו אותו בשרת שלכם, למשל ב-BOTGG_WEBHOOK_SECRET. - שלחו אירוע בדיקה מהקונסולה: הוא מגיע מסוג
webhook.test. ודאו שנקודת הקצה מאמתת את החתימה ומחזירה2xx.
- נקודות קצה לשרת: 1 ב-Free, 10 ב-Pro.
- הכתובת חייבת להיות HTTPS ציבורית בלי שם משתמש או סיסמה; כתובות פרטיות ופנימיות נדחות.
- החלפת הסוד נכנסת לתוקף מיד — פרסו את הסוד החדש בשרת שלכם מיד אחרי ההחלפה.
איך נראה משלוח
כל משלוח הוא POST עם גוף JSON בצורה { id, type, createdAt, guildId, data } — data תלוי בסוג האירוע (ראו אירועים) — ועם הכותרות האלה:
| כותרת | תיאור |
|---|---|
BotGG-Webhook-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 לא עוקב אחרי הפניות) — נחשב לניסיון שנכשל. - משלוחים שנכשלו נשלחים שוב אחרי דקה, 5 דקות, 30 דקות, שעתיים, 6 שעות ו-24 שעות — עד 7 ניסיונות בסך הכול — עם אותו
idשל האירוע. - משלוחים יכולים להגיע יותר מפעם אחת ולא לפי הסדר: סננו כפילויות לפי
idוקבעו סדר לפיcreatedAt. - נקודת קצה שממשיכה להיכשל זמן רב (יום או יותר) מושבתת אוטומטית. אחרי התיקון הפעילו אותה מחדש בקונסולה — שם אפשר גם לשלוח שוב משלוחים שנכשלו.
אימות החתימה
תמיד בדקו שהבקשה באמת הגיעה מ-BotGG לפני שאתם סומכים עליה. החתימה היא HMAC-SHA256 בהקסה של כותרת הזמן, נקודה והגוף הגולמי של הבקשה, עם הסוד של נקודת הקצה: 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: שדות יכולים להתווסף, אבל לא נמחקים ולא משנים סוג — פשוט התעלמו משדות שאתם לא מכירים. מזהי דיסקורד הם מחרוזות; מזהי רשומות של 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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
actorId | string | null | כן | Discord snowflake 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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
panelId | integer | null | כן | לפחות 0 |
channelId | string | null | כן | Discord snowflake 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 מזהה דיסקורד (מחרוזת) |
panelId | integer | null | כן | לפחות 0 |
channelId | string | null | כן | Discord snowflake id מזהה דיסקורד (מחרוזת) |
closedBy | string | null | כן | Discord snowflake 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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
messageId | string | null | כן | Discord snowflake id מזהה דיסקורד (מחרוזת) |
hostId | string | null | כן | Discord snowflake 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 מזהה דיסקורד (מחרוזת) |
moderatorId | string | null | כן | Discord snowflake 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 מזהה דיסקורד (מחרוזת) |
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 מזהה דיסקורד (מחרוזת) |
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"
}
}