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
¿Qué es el Pingback?

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
¿Cómo funciona?
  1. 1Un usuario hace clic en "Votar" en la página de tu hotel.
  2. 2HabboLibre registra el voto en el ranking.
  3. 3HabboLibre envía automáticamente un POST a tu Pingback URL con los datos del voto.
  4. 4Tu servidor verifica el secret, identifica al usuario y le da la recompensa.
  5. 5Si tu servidor no responde, HabboLibre reintenta 3 veces (30s, 2min, 5min).
Configuración
  1. 1Ve a Dashboard → Mis Hoteles → Editar tu hotel.
  2. 2Configura el campo URL de Pingback con la URL de tu servidor (ej: https://mihotel.com/api/vote-reward).
  3. 3Copia tu Pingback Secret (se genera automáticamente) y guárdalo en tu servidor.
  4. 4Haz clic en "Probar Pingback" para verificar que tu servidor recibe los datos.
Datos que recibe tu servidor

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"
}
CampoTipoDescripción
vote_idintID único del voto
tokenstringToken del voto (64 caracteres)
hotel_slugstringSlug de tu hotel
hotel_namestringNombre de tu hotel
usernamestring|nullNombre del votante en HabboLibre (null si es anonimo)
hotel_usernamestring|nullNombre del jugador en tu hotel (del parametro ?u= del link de votacion)
user_idint|nullID del votante en HabboLibre
ipstringIP del votante
voted_atISO 8601Fecha y hora del voto
secretstringTu pingback secret — verifica siempre
testboolSolo presente en pingbacks de prueba
¿Qué debe responder tu servidor?

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 OK

Reintento

HTTP 500, 502, 503, timeout
Ejemplos de Código

PHP (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'
Seguridad
  • Compara el secret con timing-safe — usa hash_equals($mySecret, $data['secret']) (PHP) o crypto.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_id ya procesados en tu DB con UNIQUE. Si recibes uno duplicado, devuelve 200 sin re-acreditar.
  • HTTPS obligatorio — el pingback_url rechaza http:// y URLs privadas (localhost, 127.0.0.1, 10/8, 172.16/12, 192.168/16). Está enforced server-side por NetworkGuard con 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/report y /votes/search aceptan 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 secret antes de loguear el payload completo.
Solución de problemas

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 viene null — es by-design.
  • Si tu servidor lee $_POST, no funcionará — el body es JSON. Usa json_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_url está configurado en Dashboard → Mis Hoteles → Editar.
  • Usa el botón "Probar pingback" en el editor del hotel — envía un payload simulado con test: true y 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_id como deduplication key. Crea un índice UNIQUE en processed_votes(vote_id) y haz INSERT IGNORE o ON 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.