API de Votos — Guía de Integración
Esta guía cubre los dos métodos para integrar tu hotel con HabboLibre y dar recompensas a tus jugadores cuando voten: Pingback (HabboLibre llama a tu servidor) o Incentive (tu servidor consulta nuestra API). Elige uno — no ambos.
Pingback vs Incentive — Comparación
Pingback Recomendado
- HabboLibre llama a tu servidor (push)
- Latencia: < 1 segundo desde el voto
- Sin polling, sin cron jobs
- 3 reintentos automáticos
- Requiere endpoint público HTTPS
- Tu servidor debe ser idempotente
Incentive (Polling)
- Tu servidor consulta nuestra API (pull)
- Funciona detrás de firewall / sin IP pública
- Batch fetch (hasta 100 votos por request)
- Búsqueda por usuario / IP / fecha
- Latencia: depende de tu cron (1-5 min típico)
- Necesitas cron job o worker
Con el sistema de Pingback, HabboLibre envía automáticamente los datos del voto a tu servidor justo después de que un usuario vota. Tu servidor solo necesita recibir el POST y dar la recompensa. No necesitas llamar a ningún endpoint.
¿Por qué Pingback?
- Tu servidor no necesita hacer requests a HabboLibre
- Funciona automáticamente — configura y olvida
- Reintentos automáticos si tu servidor no responde (3 intentos)
- Incluye un secreto compartido para verificar autenticidad
- 1Un usuario hace clic en "Votar" en la página de tu hotel.
- 2HabboLibre registra el voto en el ranking.
- 3HabboLibre envía automáticamente un
POSTa tu Pingback URL con los datos del voto. - 4Tu servidor verifica el secret, identifica al usuario y le da la recompensa.
- 5Si tu servidor no responde, HabboLibre reintenta 3 veces (30s, 2min, 5min).
- 1Ve a Dashboard → Mis Hoteles → Editar tu hotel.
- 2Configura el campo
URL de Pingbackcon la URL de tu servidor (ej:https://mihotel.com/api/vote-reward). - 3Copia tu Pingback Secret (se genera automáticamente) y guárdalo en tu servidor.
- 4Haz clic en "Probar Pingback" para verificar que tu servidor recibe los datos.
Genera un link personalizado para cada jugador de tu hotel. El parametro ?u= identifica al jugador y se envia en el pingback como hotel_username.
https://www.habbolibre.org/hotels/mi-hotel/vote?u=NombreDelJugador
Ejemplo: Si el jugador "Carlos" quiere votar por tu hotel "MiHotel":
Cuando Carlos vote, tu servidor recibira el pingback con "hotel_username": "Carlos" y podras darle la recompensa.
HabboLibre envía un POST con JSON a tu pingback_url. Headers exactos:
POST /tu-endpoint HTTP/1.1 Host: tuhotel.com Content-Type: application/json Accept: application/json User-Agent: GuzzleHttp/7 PHP/8.3
⚠️ Importante: el body es JSON, no form-urlencoded
PHP no popula $_POST con bodies JSON — solo con application/x-www-form-urlencoded y multipart/form-data. Si lees $_POST['hotel_username'] verás NULL aunque el campo SÍ haya llegado. Usa json_decode(file_get_contents('php://input'), true). En Laravel, $request->input() lo maneja correctamente.
Body JSON completo:
{
"vote_id": 1234,
"token": "abc123...def456",
"hotel_slug": "mi-hotel",
"hotel_name": "Mi Hotel",
"username": "Player123",
"hotel_username": "JuanDelHotel",
"user_id": 42,
"ip": "203.0.113.50",
"voted_at": "2026-03-17T15:30:00.000Z",
"secret": "tu_pingback_secret_aqui"
}| Campo | Tipo | Descripción |
|---|---|---|
| vote_id | int | ID único del voto |
| token | string | Token del voto (64 caracteres) |
| hotel_slug | string | Slug de tu hotel |
| hotel_name | string | Nombre de tu hotel |
| username | string|null | Nombre del votante en HabboLibre (null si es anonimo) |
| hotel_username | string|null | Nombre del jugador en tu hotel (del parametro ?u= del link de votacion) |
| user_id | int|null | ID del votante en HabboLibre |
| ip | string | IP del votante |
| voted_at | ISO 8601 | Fecha y hora del voto |
| secret | string | Tu pingback secret — verifica siempre |
| test | bool | Solo presente en pingbacks de prueba |
Tu servidor debe responder con un código HTTP 2xx (200, 201, 202, 203 o 204) para indicar que recibió el voto correctamente. Si responde con otro código, HabboLibre reintentará hasta 3 veces.
Exitoso
HTTP 200 OKReintento
HTTP 500, 502, 503, timeoutPHP (Laravel / Vanilla)
// Endpoint: POST /api/vote-reward
// CRITICAL: php://input, no $_POST — el body es JSON
$data = json_decode(file_get_contents('php://input'), true);
if (!$data || empty($data['secret'])) {
http_response_code(400);
exit('Invalid payload');
}
// 1. Verificar secret con timing-safe comparison
$mySecret = getenv('HABBOLIBRE_PINGBACK_SECRET');
if (!hash_equals($mySecret, $data['secret'])) {
http_response_code(403);
exit('Invalid secret');
}
// 2. Ignorar votos de prueba (opcional pero recomendado)
if (!empty($data['test'])) {
http_response_code(200);
exit('Test OK');
}
// 3. Idempotencia: rechaza vote_id ya procesado
// (asumes tabla processed_votes(vote_id INT UNIQUE))
if (alreadyProcessed($data['vote_id'])) {
http_response_code(200); // Devuelve OK, no reintentaremos
exit('Already processed');
}
// 4. Dar recompensa
// Prefer hotel_username (nombre en TU hotel, del link ?u=)
// Fallback a username (cuenta de HabboLibre)
$player = $data['hotel_username'] ?? $data['username'];
if ($player) {
giveReward($player, 'vote_reward');
markProcessed($data['vote_id']);
}
http_response_code(200);
echo 'OK';Node.js (Express)
app.post('/api/vote-reward', (req, res) => {
const { secret, hotel_username, username, test } = req.body;
// 1. Verificar el secreto
if (secret !== process.env.PINGBACK_SECRET) {
return res.status(403).json({ error: 'Invalid secret' });
}
// 2. Ignorar votos de prueba
if (test) return res.json({ ok: true });
// 3. Dar recompensa (hotel_username = nombre en tu hotel)
const player = hotel_username || username;
if (player) {
giveReward(player, 'vote_reward');
}
res.json({ ok: true });
});Python (Flask)
@app.route('/api/vote-reward', methods=['POST'])
def vote_reward():
data = request.get_json()
# 1. Verificar el secreto
if data.get('secret') != MY_PINGBACK_SECRET:
return 'Invalid secret', 403
# 2. Ignorar votos de prueba
if data.get('test'):
return 'Test OK'
# 3. Dar recompensa (hotel_username = nombre en tu hotel)
player = data.get('hotel_username') or data.get('username')
if player:
give_reward(player, 'vote_reward')
return 'OK'- Compara el secret con timing-safe — usa
hash_equals($mySecret, $data['secret'])(PHP) ocrypto.timingSafeEqual()(Node) en lugar de===. La comparación corta-circuito de===filtra el secret carácter por carácter vía side-channel timing. - Idempotencia obligatoria — el mismo voto puede llegarte hasta 4 veces (1 intento + 3 reintentos si tu endpoint devolvió 5xx/timeout). Guarda los
vote_idya procesados en tu DB conUNIQUE. Si recibes uno duplicado, devuelve 200 sin re-acreditar. - HTTPS obligatorio — el
pingback_urlrechazahttp://y URLs privadas (localhost, 127.0.0.1, 10/8, 172.16/12, 192.168/16). Está enforced server-side porNetworkGuardcon resolución DNS. - Reintentos — tu endpoint tiene 10 segundos de timeout. Si devuelves 5xx o no respondes, reintentamos a los 30s, 2 min y 5 min (3 intentos total). Después de eso el voto queda como fallido pero permanece disponible vía Incentive API por 30 días.
- Rate limit Incentive API —
/votes/reporty/votes/searchaceptan máximo 60 requests/minuto por IP. Polling cada 1-5 min va sobrado. - No expongas el secret en logs — al hacer debugging, redacta el campo
secretantes de loguear el payload completo.
El campo hotel_username llega vacío / no aparece
- Confirma que el voto se hizo desde
/hotels/{slug}/vote?u=PlayerName. Si el usuario votó desde la página normal del hotel (/hotels/{slug}), el campo vienenull— es by-design. - Si tu servidor lee
$_POST, no funcionará — el body es JSON. Usajson_decode(file_get_contents('php://input'), true). - Tip de debugging: loguea el raw body temporalmente:
file_put_contents('/tmp/pingback.log', file_get_contents('php://input').PHP_EOL, FILE_APPEND). Confirma exactamente qué campos llegan.
No recibo el pingback en absoluto
- Verifica que
pingback_urlestá configurado en Dashboard → Mis Hoteles → Editar. - Usa el botón "Probar pingback" en el editor del hotel — envía un payload simulado con
test: truey muestra el HTTP status de tu servidor. - Tu URL debe ser HTTPS pública (no
localhost, no IPs privadas) — está validado server-side por seguridad. - Revisa que tu firewall/WAF no esté bloqueando requests desde nuestra IP (Cloudflare ↔ tu servidor).
Mi endpoint devuelve 200 pero los reintentos siguen llegando
- El primer voto sí cuenta, pero si tarda > 10 segundos en responder, igual contamos timeout y reintentamos.
- Responde 200 lo más rápido posible, idealmente sin esperar a operaciones lentas (BD, emails). Procesa async si hace falta.
Estoy recibiendo votos duplicados
- Es esperado — tu servidor debe ser idempotente usando
vote_idcomo deduplication key. Crea un índice UNIQUE enprocessed_votes(vote_id)y hazINSERT IGNOREoON DUPLICATE KEY UPDATE.
Cómo probar manualmente con curl
curl -X POST https://tuhotel.com/api/vote-reward \
-H "Content-Type: application/json" \
-d '{"vote_id":1,"token":"test","hotel_slug":"mi-hotel","hotel_name":"Mi Hotel","username":"Player","hotel_username":"JuanDelHotel","user_id":1,"ip":"127.0.0.1","voted_at":"2026-05-12T00:00:00Z","secret":"TU_SECRET_AQUI","test":true}'Debe devolver HTTP 200 OK.
