REST API v1 · למפתחים

Webhooks: אירועי שרת הדיסקורד ישר לשרת שלכם

במקום לתשאל את ה-API שוב ושוב, תנו ל-BotGG לפנות אליכם: כשמשהו קורה בשרת הדיסקורד — שינוי יתרה, עלייה ברמה, רכישה, כרטיס חדש — BotGG שולח בקשת POST חתומה עם JSON לנקודת הקצה שלכם ב-HTTPS.

תיעוד למפתחים
בעמוד הזה

הגדרת נקודת קצה

  1. בקונסולה, פתחו את אזור ה-API של השרת והוסיפו נקודת קצה ל-Webhook: כתובת https:// והאירועים שאתם רוצים.
  2. העתיקו את סוד החתימה (whsec_…) — הוא מוצג פעם אחת בלבד — ושמרו אותו בשרת שלכם, למשל ב-BOTGG_WEBHOOK_SECRET.
  3. שלחו אירוע בדיקה מהקונסולה: הוא מגיע מסוג 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>")>.

  1. קראו את הגוף בדיוק כפי שהתקבל (לפני כל פענוח JSON).
  2. דחו את הבקשה אם BotGG-Webhook-Timestamp רחוק ביותר מ-5 דקות מהשעון שלכם — זה מונע שליחה חוזרת זדונית.
  3. חשבו HMAC של <timestamp>.<הגוף הגולמי> עם הסוד שלכם והשוו אותו, בזמן קבוע, לכל ערך v1= בכותרת BotGG-Signature.
Node.js (Express)
// 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 (Flask)
# 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 (תנועות, רכישות, כרטיסים…) הם מספרים שלמים.

אירועי Webhook
אירועפיצ׳רתיאור
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 תנועה בבנק
שדות `data` של bank.transaction
שדהסוגחובהתיאור
transactionIdintegerכןלפחות 0
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
typestringכןLedger type: daily, transfer_in, transfer_out, fee, admin_add, admin_remove, admin_set, flow, api, …
amountintegerכןSigned change (negative = debit)
balanceAfterintegerכן
reasonstring | nullכן
counterpartyIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
actorIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
createdAtstringכן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 עלייה ברמה
שדות `data` של levels.levelUp
שדהסוגחובהתיאור
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
levelintegerכןלפחות 0
previousLevelintegerכןלפחות 0
sourcestringכןאחד מ: 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 רכישה
שדות `data` של shop.purchase
שדהסוגחובהתיאור
purchaseIdintegerכןלפחות 0
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
itemobjectכן
item.idinteger | nullכןnull when the item was deleted since
לפחות 0
item.namestringכן
item.emojistring | nullכן
item.typestringכןrole | color | item | …
storeobjectכןid/name null = the built-in Main store
store.idinteger | nullכןלפחות 0
store.namestring | nullכן
priceintegerכןלפחות 0
statusstringכןowned | used | refunded
roleIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
expiresAtstring | nullכןISO-8601 timestamp (UTC)
תאריך ושעה ב-ISO 8601 (UTC)
createdAtstringכן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 החזר
שדות `data` של shop.refund
שדהסוגחובהתיאור
purchaseIdintegerכןלפחות 0
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
itemobjectכן
item.idinteger | nullכןnull when the item was deleted since
לפחות 0
item.namestringכן
item.emojistring | nullכן
item.typestringכןrole | color | item | …
priceintegerכןלפחות 0
refundedBystring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
refundedAtstringכן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 כרטיס נפתח
שדות `data` של tickets.opened
שדהסוגחובהתיאור
ticketIdintegerכןלפחות 0
numberintegerכן
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
panelIdinteger | nullכןלפחות 0
channelIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
subjectstring | nullכן
openedAtstringכן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 כרטיס נסגר
שדות `data` של tickets.closed
שדהסוגחובהתיאור
ticketIdintegerכןלפחות 0
numberintegerכן
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
panelIdinteger | nullכןלפחות 0
channelIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
closedBystring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
closeReasonstring | nullכן
closeTypestring | nullכןmanual | auto | leave | …
openedAtstring | nullכןISO-8601 timestamp (UTC)
תאריך ושעה ב-ISO 8601 (UTC)
closedAtstringכן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 חבר הצטרף
שדות `data` של members.joined
שדהסוגחובהתיאור
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
botbooleanכן
joinedAtstringכןISO-8601 timestamp (UTC)
תאריך ושעה ב-ISO 8601 (UTC)
accountCreatedAtstring | 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 חבר עזב
שדות `data` של members.left
שדהסוגחובהתיאור
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
botbooleanכן
leftAtstringכן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 הגרלה הסתיימה
שדות `data` של giveaways.ended
שדהסוגחובהתיאור
giveawayIdintegerכןלפחות 0
prizestringכן
channelIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
messageIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
hostIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
winnerIdsarray of stringכן
entrantsintegerכןלפחות 0
endedAtstringכן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 תיק ניהול
שדות `data` של moderation.caseCreated
שדהסוגחובהתיאור
caseIdintegerכןלפחות 0
numberintegerכן
actionstringכןwarn | timeout | kick | ban | tempban | unban | …
targetIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
moderatorIdstring | nullכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
reasonstring | nullכן
durationSecondsinteger | nullכןלפחות 0
sourcestring | nullכן
expiresAtstring | nullכןISO-8601 timestamp (UTC)
תאריך ושעה ב-ISO 8601 (UTC)
createdAtstringכן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 מועמדות נשלחה
שדות `data` של applications.submitted
שדהסוגחובהתיאור
submissionIdintegerכןלפחות 0
numberintegerכן
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
formIdinteger | nullכןלפחות 0
formTitlestring | nullכן
statusstringכן
answersarray of objectכן
answers[].questionIdstringכן
answers[].labelstringכן
answers[].typestringכןQuestion type, e.g. short, paragraph, select, number, yesno
answers[].valuestring | nullכן
answers[].textstring | nullכןHuman-readable answer (as shown in Discord), or null
submittedAtstringכן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 ערעור נשלח
שדות `data` של appeals.submitted
שדהסוגחובהתיאור
appealIdintegerכןלפחות 0
numberintegerכן
userIdstringכןDiscord snowflake id
מזהה דיסקורד (מחרוזת)
banCaseNumberinteger | nullכן
banReasonstring | nullכן
answersarray of objectכן
answers[].questionIdstringכן
answers[].labelstringכן
answers[].typestringכןQuestion type, e.g. short, paragraph, select, number, yesno
answers[].valuestring | nullכן
answers[].textstring | nullכןHuman-readable answer (as shown in Discord), or null
createdAtstringכן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"
  }
}

שאלות נפוצות

צריך מפתח API כדי לקבל Webhooks?

לא. ה-Webhooks מאומתים בחתימה שנעשית עם סוד החתימה של נקודת הקצה. מפתח API נדרש רק אם השרת שלכם גם קורא ל-REST API.

למה בדיקת החתימה נכשלת?

בדרך כלל כי הגוף פוענח ונכתב מחדש לפני החישוב. חשבו את ה-HMAC על הבייטים הגולמיים שהתקבלו, עם סוד ה-whsec_… של נקודת הקצה הזו, ובדקו את השעון של השרת.

מה קורה כשהשרת שלי לא זמין?

BotGG שולח כל משלוח שוב עד 7 פעמים במשך כ-33 שעות. אם נקודת קצה ממשיכה להיכשל זמן רב היא מושבתת; אחרי התיקון אפשר לשלוח שוב מהקונסולה משלוחים שנכשלו.

מוכנים לשדרג את השרת?

מוסיפים את BotGG בחינם ומפעילים יכולות בקונסולת הווב.