How to create a Top.gg webhook
How to Create a Top.gg Webhook on Square Cloud
Webhooks are fundamental for integrating your bot with external services. Whenever a user votes for your bot, Top.gg sends a POST request to your application. On Square Cloud, you can host this webhook stably on port 80.
This guide uses Top.gg v1 webhooks. Each request is signed with the
x-topgg-signatureheader (formatt=<timestamp>,v1=<signature>), where the signature is an HMAC SHA-256 of<timestamp>.<raw body>made with yourwhs_secret. The legacy webhook, which sent a password in theAuthorizationheader, is not covered here.
1. How Does the Communication Work?
- User votes on Top.gg.
- Top.gg sends a signed JSON request to your application's URL.
- Your Application recomputes the signature with your secret, rejects anything that doesn't match and processes the vote (
data.user.platform_idis the voter's Discord ID).
2. Implementation in JavaScript (Express)
Install Express and enable the import syntax:
npm pkg set type=module
npm install express
The example verifies the signature with Node's built-in crypto module, so no Top.gg package is needed (in our tests, the listener of @top-gg/sdk 4.0.0 answered valid deliveries with a timeout, which makes Top.gg retry them).
import crypto from "node:crypto";
import express from "express";
const app = express();
// The whs_ secret from your project's Webhooks page
const SECRET = process.env.TOPGG_WEBHOOK_SECRET;
function isValidSignature(rawBody, header = "") {
// Header format: t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
const parts = Object.fromEntries(header.split(",").map((item) => item.split("=")));
const timestamp = Number(parts.t);
if (!SECRET || !Buffer.isBuffer(rawBody) || !parts.v1 || !timestamp) return false;
// Reject requests older than 5 minutes (replay protection)
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto.createHmac("sha256", SECRET).update(`${parts.t}.`).update(rawBody).digest();
const received = Buffer.from(parts.v1, "hex");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
// express.raw keeps the exact bytes that were signed
app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
if (!isValidSignature(req.body, req.get("x-topgg-signature"))) return res.sendStatus(401);
const { type, data } = JSON.parse(req.body);
// "vote.create" is a real vote, "webhook.test" comes from the Send Test button
if (type === "vote.create" || type === "webhook.test") {
console.log(`Vote received! User ID: ${data.user.platform_id}`);
}
res.sendStatus(204);
});
// Square Cloud sets PORT=80
const port = Number(process.env.PORT) || 80;
app.listen(port, "0.0.0.0", () => console.log(`Listening on port ${port}`));
3. Implementation in Python (Flask)
Install the libraries with pip install flask waitress (Waitress is a production server; don't use Flask's development server).
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
from waitress import serve
app = Flask(__name__)
# The whs_ secret from your project's Webhooks page
SECRET = os.environ.get("TOPGG_WEBHOOK_SECRET", "")
def is_valid_signature(raw_body: bytes, header: str) -> bool:
# Header format: t=<unix timestamp>,v1=<hex HMAC-SHA256 of "<timestamp>.<raw body>">
try:
parts = dict(item.split("=", 1) for item in header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
age = abs(time.time() - int(timestamp))
except (KeyError, ValueError):
return False
if age > 300: # reject requests older than 5 minutes (replay protection)
return False
message = timestamp.encode() + b"." + raw_body
expected = hmac.new(SECRET.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), signature.encode())
@app.route("/webhook", methods=["POST"])
def topgg_webhook():
raw_body = request.get_data()
if not SECRET or not is_valid_signature(raw_body, request.headers.get("x-topgg-signature", "")):
abort(401)
payload = json.loads(raw_body)
# "vote.create" is a real vote, "webhook.test" comes from the Send Test button
if payload["type"] in ("vote.create", "webhook.test"):
print(f'Vote received! User ID: {payload["data"]["user"]["platform_id"]}')
return "", 204
if __name__ == "__main__":
# Square Cloud sets PORT=80
serve(app, host="0.0.0.0", port=int(os.environ.get("PORT", 80)))
4. Configuration on Square Cloud
For the webhook to work, you must configure your application as a website:
- Deployment: Upload your code (remember not to send
node_modulesorvenv). - Web Publication: In the upload form, enable the "Web Publication" option on the first upload and define the subdomain (or set
SUBDOMAIN=in your config file). Websites need at least 512 MB of RAM. - Subdomain: Define a name (e.g.,
topgg-webhook). Your webhook URL will betopgg-webhook.squareweb.app/webhook, over HTTPS. - Environment Variables: Add the
TOPGG_WEBHOOK_SECRETkey with thewhs_secret generated by Top.gg (next step).
5. Configuration on the Top.gg Panel
- Open your project's dashboard on Top.gg and go to Webhooks.
- Paste your webhook URL and save.
- Top.gg generates a secret that starts with
whs_: copy it into theTOPGG_WEBHOOK_SECRETenvironment variable on Square Cloud and keep it private. - Click Send Test and check that the vote appears in your application logs.
6. Best Practices
- Security: Never expose the secret in your code. Always use environment variables.
- Logs: Keep logs of received requests to be able to debug if a vote is not processed.
- Error Handling: Always respond quickly (
204or200). If your reward logic is time-consuming, process it asynchronously to avoid a timeout, which makes Top.gg retry the delivery.
For more details, see the Square Cloud Top.gg tutorial.
Updated on: 10/04/2026
Thank you!
