Cómo crear un webhook de Top.gg
Cómo crear un webhook de Top.gg en Square Cloud
Los webhooks son fundamentales para integrar tu bot con servicios externos. Cada vez que un usuario vota por tu bot, Top.gg envía una petición POST a tu aplicación. En Square Cloud, puedes alojar ese webhook de forma estable en el puerto 80.
Esta guía usa los webhooks v1 de Top.gg. Cada petición llega firmada en la cabecera
x-topgg-signature(formatot=<timestamp>,v1=<firma>), donde la firma es un HMAC SHA-256 de<timestamp>.<cuerpo crudo>hecho con tu secretowhs_. El webhook legado, que enviaba una contraseña en la cabeceraAuthorization, no se cubre aquí.
1. ¿Cómo funciona la comunicación?
- El usuario vota en Top.gg.
- Top.gg envía una petición JSON firmada a la URL de tu aplicación.
- Tu aplicación recalcula la firma con tu secreto, rechaza lo que no coincida y procesa el voto (
data.user.platform_ides el ID de Discord de quien votó).
2. Implementación en JavaScript (Express)
Instala Express y activa la sintaxis import:
npm pkg set type=module
npm install express
El ejemplo verifica la firma con el módulo nativo crypto de Node, sin ningún paquete de Top.gg (en nuestras pruebas, el listener de @top-gg/sdk 4.0.0 respondió entregas válidas con timeout, lo que hace que Top.gg las reenvíe).
import crypto from "node:crypto";
import express from "express";
const app = express();
// El secreto whs_ de la página Webhooks de tu proyecto
const SECRET = process.env.TOPGG_WEBHOOK_SECRET;
function isValidSignature(rawBody, header = "") {
// Formato de la cabecera: t=<timestamp unix>,v1=<HMAC-SHA256 en hex de "<timestamp>.<cuerpo crudo>">
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;
// Rechaza peticiones con más de 5 minutos (protección contra replay)
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 mantiene exactamente los bytes que fueron firmados
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" es un voto real; "webhook.test" viene del botón Send Test
if (type === "vote.create" || type === "webhook.test") {
console.log(`¡Voto recibido! ID del usuario: ${data.user.platform_id}`);
}
res.sendStatus(204);
});
// Square Cloud define PORT=80
const port = Number(process.env.PORT) || 80;
app.listen(port, "0.0.0.0", () => console.log(`Escuchando en el puerto ${port}`));
3. Implementación en Python (Flask)
Instala las librerías con pip install flask waitress (Waitress es un servidor de producción; no uses el servidor de desarrollo de Flask).
import hashlib
import hmac
import json
import os
import time
from flask import Flask, abort, request
from waitress import serve
app = Flask(__name__)
# El secreto whs_ de la página Webhooks de tu proyecto
SECRET = os.environ.get("TOPGG_WEBHOOK_SECRET", "")
def is_valid_signature(raw_body: bytes, header: str) -> bool:
# Formato de la cabecera: t=<timestamp unix>,v1=<HMAC-SHA256 en hex de "<timestamp>.<cuerpo crudo>">
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: # rechaza peticiones con más de 5 minutos (protección contra replay)
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" es un voto real; "webhook.test" viene del botón Send Test
if payload["type"] in ("vote.create", "webhook.test"):
print(f'¡Voto recibido! ID del usuario: {payload["data"]["user"]["platform_id"]}')
return "", 204
if __name__ == "__main__":
# Square Cloud define PORT=80
serve(app, host="0.0.0.0", port=int(os.environ.get("PORT", 80)))
4. Configuración en Square Cloud
Para que el webhook funcione, debes configurar tu aplicación como un sitio:
- Deploy: Envía tu código (recuerda no enviar
node_modulesnivenv). - Publicación web: En el formulario de envío, activa la opción "Publicación en la Web" ya en el primer envío y define el subdominio (o usa
SUBDOMAIN=en el archivo de configuración). Los sitios necesitan al menos 512 MB de RAM. - Subdominio: Define un nombre (ej.:
topgg-webhook). La URL de tu webhook serátopgg-webhook.squareweb.app/webhook, con HTTPS. - Variables de entorno: Añade la clave
TOPGG_WEBHOOK_SECRETcon el secretowhs_generado por Top.gg (siguiente paso).
5. Configuración en el panel de Top.gg
- Abre el panel de tu proyecto en Top.gg y ve a Webhooks.
- Pega la URL de tu webhook y guarda.
- Top.gg genera un secreto que empieza con
whs_: cópialo en la variable de entornoTOPGG_WEBHOOK_SECRETen Square Cloud y mantenlo en secreto. - Haz clic en Send Test y comprueba que el voto aparece en los logs de tu aplicación.
6. Buenas prácticas
- Seguridad: Nunca expongas el secreto en el código. Usa siempre variables de entorno.
- Logs: Mantén logs de las peticiones recibidas para depurar si un voto no se procesa.
- Manejo de errores: Responde siempre rápido (
204o200). Si la lógica de recompensa es lenta, procésala de forma asíncrona para evitar el timeout, que hace que Top.gg reenvíe la entrega.
Para más detalles, mira el tutorial de Top.gg de Square Cloud.
Actualizado el: 04/10/2026
¡Gracias!
