Articles on: Websites & APIs
This article is also available in:

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-signature header (format t=<timestamp>,v1=<signature>), where the signature is an HMAC SHA-256 of <timestamp>.<raw body> made with your whs_ secret. The legacy webhook, which sent a password in the Authorization header, is not covered here.



1. How Does the Communication Work?


  1. User votes on Top⁠.gg.
  2. Top⁠.gg sends a signed JSON request to your application's URL.
  3. Your Application recomputes the signature with your secret, rejects anything that doesn't match and processes the vote (data.user.platform_id is 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:


  1. Deployment: Upload your code (remember not to send node_modules or venv).
  2. 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.
  3. Subdomain: Define a name (e.g., topgg-webhook). Your webhook URL will be topgg-webhook⁠.squareweb⁠.app/webhook, over HTTPS.
  4. Environment Variables: Add the TOPGG_WEBHOOK_SECRET key with the whs_ secret generated by Top⁠.gg (next step).



5. Configuration on the Top⁠.gg Panel


  1. Open your project's dashboard on Top⁠.gg and go to Webhooks.
  2. Paste your webhook URL and save.
  3. Top⁠.gg generates a secret that starts with whs_: copy it into the TOPGG_WEBHOOK_SECRET environment variable on Square Cloud and keep it private.
  4. 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 (204 or 200). 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

Was this article helpful?

Share your feedback

Cancel

Thank you!