IdleScore

Documentação da API

Tudo que seu jogo precisa para receber votos e entregar recompensa automaticamente.

Como a integração funciona

  1. Você leva o jogador para o link de votação, com o identificador dele.
  2. O IdleScore autentica a pessoa, valida a elegibilidade e registra o voto.
  3. Seu endpoint recebe um evento assinado vote.confirmed.
  4. Seu jogo entrega a recompensa.
Não existe endpoint que permita ao seu servidor registrar um voto diretamente. Todo voto passa pela autenticação e pela validação anti-fraude do IdleScore. Isso protege o seu ranking tanto quanto o de todo mundo.

Credenciais

Em Painel → seu jogo → Integração você gera:

  • Chave pública (pk_live_…) — vai na URL de votação, não é segredo.
  • Chave secreta (sk_live_…) — autentica chamadas de servidor. Aparece uma única vez.
  • Segredo de webhook (whsec_…) — assina os eventos que enviamos. Também aparece uma vez só.

Guardamos apenas o hash da chave secreta. Se você perder, precisa rotacionar — nem nós conseguimos recuperá-la.

Link de votação

text
https://idlescore.com/votar/SEU-SLUG?ref=CHAVE_PUBLICA&player=PLAYER_ID
  • SEU-SLUG — o identificador do seu jogo no IdleScore
  • ref — sua chave pública, para vincular o voto à sua integração
  • player — o identificador do jogador no seu jogo

O jogador confirma o identificador antes de votar e vê exatamente o que será enviado a você. Nunca coloque senha, token, chave privada ou dado de pagamento nesse parâmetro.

Webhook de voto

A cada voto confirmado enviamos um POST para seu endpoint:

json
{
  "id": "evt_123",
  "type": "vote.confirmed",
  "createdAt": "2026-08-21T15:00:00.000Z",
  "data": {
    "gameId": "game_123",
    "gameSlug": "nome-do-jogo",
    "voteId": "vote_123",
    "playerIdentifier": "player_456",
    "votedAt": "2026-08-21T15:00:00.000Z"
  }
}

Cabeçalhos

text
X-IdleScore-Signature: sha256=ASSINATURA
X-IdleScore-Timestamp: TIMESTAMP_UNIX
X-IdleScore-Event-Id: evt_123
Idempotency-Key: vote_123

Verificação da assinatura

text
HMAC-SHA256(timestamp + "." + corpo_bruto, webhookSecret)

Quatro cuidados que fazem diferença:

  • Use o corpo bruto, antes de qualquer parse. Reserializar o JSON muda os bytes e quebra a assinatura.
  • Compare em tempo constante. Comparação com === vaza informação pelo tempo de resposta.
  • Recuse timestamp fora da janela de 5 minutos.
  • Trate Idempotency-Key: o mesmo voto pode chegar mais de uma vez.

Node.js

javascript
import express from 'express'
import { createHmac, timingSafeEqual } from 'node:crypto'

const app = express()
const SEGREDO = process.env.IDLESCORE_WEBHOOK_SECRET
const processados = new Set() // em produção, use seu banco

// O corpo precisa chegar como Buffer, não como objeto já parseado.
app.post('/idlescore/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const assinatura = req.get('X-IdleScore-Signature') ?? ''
  const timestamp = req.get('X-IdleScore-Timestamp') ?? ''
  const chaveIdempotencia = req.get('Idempotency-Key') ?? ''

  // 1. Janela de tempo
  const idade = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!Number.isFinite(idade) || idade > 300) return res.status(400).send('timestamp fora da janela')

  // 2. Assinatura, em tempo constante
  const esperada = 'sha256=' + createHmac('sha256', SEGREDO)
    .update(timestamp + '.' + req.body.toString('utf8'))
    .digest('hex')

  const a = Buffer.from(esperada)
  const b = Buffer.from(assinatura)
  if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(401).send('assinatura invalida')

  // 3. Idempotência: nunca entregue a mesma recompensa duas vezes
  if (processados.has(chaveIdempotencia)) return res.status(200).send('ja processado')
  processados.add(chaveIdempotencia)

  // 4. Responda rápido e processe depois
  res.status(200).send('ok')

  const evento = JSON.parse(req.body.toString('utf8'))
  if (evento.type === 'vote.confirmed') {
    entregarRecompensa(evento.data.playerIdentifier, evento.data.voteId).catch(console.error)
  }
})

PHP

php
<?php
$segredo = getenv('IDLESCORE_WEBHOOK_SECRET');

// Corpo bruto, sem parse
$corpo = file_get_contents('php://input');
$assinatura = $_SERVER['HTTP_X_IDLESCORE_SIGNATURE'] ?? '';
$timestamp  = $_SERVER['HTTP_X_IDLESCORE_TIMESTAMP'] ?? '';
$idempotencia = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? '';

// 1. Janela de tempo
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit('timestamp fora da janela');
}

// 2. Assinatura em tempo constante
$esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpo, $segredo);
if (!hash_equals($esperada, $assinatura)) {
    http_response_code(401);
    exit('assinatura invalida');
}

// 3. Idempotência
$pdo = new PDO(getenv('DATABASE_DSN'));
$stmt = $pdo->prepare('INSERT IGNORE INTO idlescore_eventos (chave) VALUES (?)');
$stmt->execute([$idempotencia]);
if ($stmt->rowCount() === 0) {
    http_response_code(200);
    exit('ja processado');
}

// 4. Responda rápido
http_response_code(200);
echo 'ok';
if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); }

$evento = json_decode($corpo, true);
if (($evento['type'] ?? '') === 'vote.confirmed') {
    entregarRecompensa($evento['data']['playerIdentifier'], $evento['data']['voteId']);
}

Python

python
import hmac, hashlib, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SEGREDO = os.environ['IDLESCORE_WEBHOOK_SECRET'].encode()

@app.post('/idlescore/webhook')
def webhook():
    corpo = request.get_data()  # bytes crus, sem parse
    assinatura = request.headers.get('X-IdleScore-Signature', '')
    timestamp = request.headers.get('X-IdleScore-Timestamp', '')
    idempotencia = request.headers.get('Idempotency-Key', '')

    # 1. Janela de tempo
    try:
        if abs(time.time() - int(timestamp)) > 300:
            abort(400, 'timestamp fora da janela')
    except ValueError:
        abort(400, 'timestamp invalido')

    # 2. Assinatura em tempo constante
    esperada = 'sha256=' + hmac.new(
        SEGREDO, f'{timestamp}.'.encode() + corpo, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(esperada, assinatura):
        abort(401, 'assinatura invalida')

    # 3. Idempotência
    if ja_processado(idempotencia):
        return 'ja processado', 200
    marcar_processado(idempotencia)

    # 4. Processe fora do ciclo da requisição
    evento = request.get_json(force=True)
    if evento.get('type') == 'vote.confirmed':
        fila.enqueue(entregar_recompensa,
                     evento['data']['playerIdentifier'],
                     evento['data']['voteId'])
    return 'ok', 200

Retentativas

Se o seu endpoint não responder 2xx, tentamos de novo até 6 vezes:

text
1ª tentativa: 1 minuto depois
2ª tentativa: 5 minutos depois
3ª tentativa: 15 minutos depois
4ª tentativa: 1 hora depois
5ª tentativa: 6 horas depois
6ª tentativa: 24 horas depois

Respostas 4xx (exceto 408 e 429) não são retentadas: elas indicam erro do lado do destinatário, e repetir não resolveria. Você também pode reenviar manualmente pelo painel. Responda 2xx rápido e faça o processamento pesado de forma assíncrona.

Consulta de elegibilidade

http
GET https://idlescore.com/api/v1/games/{slug}/vote-status?player=PLAYER_ID
Authorization: Bearer SUA_CHAVE_SECRETA
json
{
  "eligible": false,
  "lastVoteAt": "2026-08-21T15:00:00.000Z",
  "nextEligibleAt": "2026-08-22T15:00:00.000Z"
}

Útil para mostrar dentro do jogo quanto falta para o próximo voto, sem chutar o horário.

Recuperação de eventos

http
GET https://idlescore.com/api/v1/votes?after=CURSOR&limit=50
Authorization: Bearer SUA_CHAVE_SECRETA
json
{
  "data": [
    { "voteId": "vote_123", "votedAt": "2026-08-21T15:00:00.000Z", "playerIdentifier": "player_456" }
  ],
  "nextCursor": "vote_123",
  "hasMore": false
}

Serve para se reconciliar depois de uma indisponibilidade, sem depender de reenvio. Depois do período de retenção, playerIdentifier vem null — minimização de dado é parte do compromisso com quem vota.

Estatísticas públicas

http
GET https://idlescore.com/api/v1/games/{slug}

Sem autenticação. Devolve apenas o que já é público na página do jogo:

json
{
  "slug": "nome-do-jogo",
  "name": "Nome do Jogo",
  "status": "RELEASED",
  "verified": true,
  "position": 3,
  "monthVotes": 8112,
  "totalVotes": 56784,
  "likes": 2044,
  "idleScore": 84.2,
  "reviewCount": 331,
  "period": "2026-08",
  "nextReset": "2026-09-01T03:00:00.000Z"
}

Limites por plano

PlanoRequisições/minEndpoints de webhook
Free601
Start1202
Pro3005
Master90020

Ao ultrapassar o limite, respondemos 429. Nenhum voto é perdido por isso.

Ambiente de teste

Nos planos Master, credenciais e endpoints de teste ficam separados dos de produção. Eventos de teste têm "isTest": true e nunca geram voto nem alteram o ranking. Em qualquer plano você pode disparar um evento de teste pelo painel.

Erros

CódigoSignificado
401Chave ausente, inválida ou revogada
403A chave não pertence a este jogo, ou o domínio não está verificado
404Jogo não encontrado
422Parâmetros inválidos
429Limite de requisições atingido
json
{ "error": "credencial-invalida", "message": "Chave inválida ou revogada." }

Boas práticas

  • Guarde o segredo de webhook em variável de ambiente, nunca no código do jogo.
  • Registre voteId como chave única antes de creditar a recompensa.
  • Responda 2xx em menos de 5 segundos e processe em fila.
  • Trate o identificador de jogador como dado pessoal: use e descarte.
  • O intervalo entre votos é de 24 horas — não prometa recompensa mais frequente que isso.
  • Rotacione as chaves se suspeitar de vazamento; a antiga é revogada na hora.

Dúvida na integração? Fale com a gente com o slug do seu jogo e o ID do evento — não precisa mandar segredo nenhum.