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.

Antes de começar: dois pré-requisitos

Sem estes dois passos nada funciona, e a falha não vem com mensagem de erro — vem como resposta vazia ou campo nulo. Resolva os dois antes de escrever código.

1. Verifique o domínio do jogo

Enquanto o domínio não estiver verificado, a API responde 403 em todos os endpoints e o link de votação responde 404. A verificação é gratuita em qualquer plano e leva minutos: em Painel → seu jogo → Integração você escolhe entre registro DNS TXT, meta tag no HTML da página inicial, ou e-mail do próprio domínio.

2. Ligue a “Recompensa por voto”

Em Painel → seu jogo → Editar existe a opção Recompensa por voto. Ela é o que liga o fluxo de identificador inteiro:

  • Ligada: a tela de votação pede o identificador do jogador e exige que ele confirme — inclusive para quem chegou pelo ranking, sem ?player= na URL. O valor chega no seu webhook em data.playerIdentifier.
  • Desligada: nós descartamos o ?player=. Ele não é gravado nem entregue, e todo evento chega com "playerIdentifier": null — sem erro e sem aviso.
Está vendo identificador nulo em todos os votos? Confira esta opção antes de qualquer outra coisa. Dá para verificar sem chave nenhuma: GET https://idlescore.com/api/v1/games/{slug} devolve hasVoteReward.

Com a opção ligada, preencha também a descrição da recompensa. É o texto que o jogador lê na hora de confirmar o identificador — é ali que ele descobre o que ganha (“500 gemas”, “1 baú lendário”). Em branco, a tela pede um identificador sem dizer para quê.

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. Hoje é apenas informativo: não altera nada no registro do voto e não é necessário para a recompensa funcionar. Pode incluir ou omitir.
  • 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"
  }
}
  • createdAt é o instante desta entrega e muda a cada retentativa. A hora do voto é data.votedAt — use essa para log, janela de campanha ou ordenação.
  • data.playerIdentifier é string | null. Vem nulo quando a “Recompensa por voto” está desligada, quando o voto é anterior a você tê-la ligado, e em /api/v1/votes depois dos 30 dias de retenção do identificador.
  • Identificador nulo não é requisição malformada: responda 2xx, registre para conferência e siga. Responder 4xx queima o evento em definitivo.

Cabeçalhos

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

A chave de idempotência é <eventId>:<endpointId> — não é o voteId. Isso importa se você cadastrar mais de um endpoint: o mesmo voto chega a cada um deles com uma chave diferente, então deduplicar só por essa chave creditaria a recompensa uma vez por endpoint. Se o seu objetivo é redundância e os endpoints caem no mesmo sistema, deduplique por data.voteId, que é o mesmo em todos.

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
  // 500, nunca 4xx: relogio dessincronizado se resolve sozinho, e 4xx
  // queima o evento em definitivo (veja "Retentativas").
  const idade = Math.abs(Date.now() / 1000 - Number(timestamp))
  if (!Number.isFinite(idade) || idade > 300) return res.status(500).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)
  // 401 so aqui: assinatura invalida e o unico caso em que repetir nao adianta.
  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
// 500, nunca 4xx: relogio dessincronizado se resolve sozinho, e 4xx
// queima o evento em definitivo (veja "Retentativas").
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(500);
    exit('timestamp fora da janela');
}

// 2. Assinatura em tempo constante
$esperada = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $corpo, $segredo);
// 401 so aqui: assinatura invalida e o unico caso em que repetir nao adianta.
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
    # 500, nunca 4xx: relogio dessincronizado se resolve sozinho, e 4xx
    # queima o evento em definitivo (veja "Retentativas").
    try:
        if abs(time.time() - int(timestamp)) > 300:
            abort(500, 'timestamp fora da janela')
    except ValueError:
        abort(500, 'timestamp invalido')

    # 2. Assinatura em tempo constante
    esperada = 'sha256=' + hmac.new(
        SEGREDO, f'{timestamp}.'.encode() + corpo, hashlib.sha256
    ).hexdigest()
    # 401 so aqui: assinatura invalida e o unico caso em que repetir nao adianta.
    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 — leia antes de escrever a resposta

Um único 4xx queima o evento para sempre

O código HTTP que você responde decide o destino do evento, já na primeira tentativa. Se não conseguiu processar agora, responda 500 — aí tentamos de novo. Ou responda 2xx e resolva do seu lado. Nunca 4xx por problema passageiro.

Sua respostaO que fazemos
2xxEntregue. Fim.
5xxRetentamos.
408 ou 429Retentamos.
Sem resposta (timeout, queda de rede, DNS)Retentamos.
400, 401, 403, 404, 422…Nunca mais tentamos.
3xxNunca mais tentamos. Não seguimos redirecionamento: cadastre a URL final.

A regra prática: 4xx quer dizer “este evento é inválido, não me mande de novo”. Use 4xx apenas quando a assinatura não confere — aí é ataque ou segredo errado, e repetir não resolveria mesmo. Para tudo que pode se resolver sozinho (banco fora do ar, jogador que ainda não existe no seu sistema, fila cheia, relógio dessincronizado), responda 500 e receba o evento de novo.

Quando retentamos, é até 6 vezes, nesta cadência:

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

Depois da última, a entrega fica como EXHAUSTED e não volta sozinha. O reenvio manual pelo painel está disponível nos planos Ascensão e Prestígio; em qualquer plano, a recuperação que sempre funciona é GET /api/v1/votes, que lista os votos do período e permite conferir o que faltou.

Responda 2xx rápido e faça o processamento pesado de forma assíncrona: se você demorar mais que o nosso tempo limite, contamos como falha e o mesmo evento chega de novo — por isso a idempotência do passo 3 importa.

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.

Não use isto para decidir se pode creditar

A resposta é sobre o identificador, e o intervalo entre votos é contado por conta IdleScore — não temos como ligar uma coisa na outra. Um jogador que já votou hoje por outra conta, ou que nunca teve identificador coletado, aparece aqui como eligible: true e mesmo assim tem o voto recusado no site.

Este endpoint responde bem a “este identificador já recebeu recompensa?”. Para “esta pessoa consegue votar agora?”, não há resposta possível pela API — quem decide é o registro do voto. Credite quando o vote.confirmed chegar, nunca antes.

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.

Saúde da integração

http
GET https://idlescore.com/api/v1/integration
Authorization: Bearer SUA_CHAVE_SECRETA
json
{
  "game": "seu-jogo",
  "plan": "PRO",
  "environment": "LIVE",
  "voteCooldownHours": 12,
  "last24h": {
    "votes": 37,
    "deliveries": { "delivered": 37, "pending": 0, "failed": 0, "blocked": 0 }
  },
  "lastSuccessAt": "2026-08-24T05:41:12.000Z",
  "lastFailure": null,
  "endpoints": [
    {
      "environment": "LIVE",
      "active": true,
      "consecutiveFailures": 0,
      "disabledReason": null,
      "lastTestAt": "2026-08-23T21:00:00.000Z"
    }
  ]
}

Quando o webhook para de chegar, do seu lado não acontece nada: seu endpoint continua no ar, seu código continua certo, e silêncio é indistinguível de "ninguém votou hoje". Sem isto, o jeito de descobrir é um jogador reclamar que votou e não recebeu — o que costuma levar dias.

Consulte de tempos em tempos e avise sua equipe quando deliveries.failedsubir ou endpoints[].active virar false. O campo lastFailure.reason traz o motivo já pronto para leitura ("tempo esgotado", "o endereço respondeu 301 (redirecionamento)"), sem corpo de resposta e sem segredo.

endpoints vazio quer dizer que não há webhook cadastrado — é a primeira coisa a conferir quando nada chega.

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
Início3002
Impulso9002
Ascensão18005
Prestígio450010

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

Evento de teste

Em qualquer plano, e sem custo, você dispara um evento de teste pelo painel do jogo, em Integração. Ele chega ao seu servidor com a mesma assinatura e a mesma estrutura de envelope de um evento real.

O que muda é o type: o evento de teste chega como webhook.test, e nunca como vote.confirmed. Distinga por aí — e trate como desconhecido qualquer type que você não espere, respondendo 2xx sem creditar nada. O data do evento de teste não traz os campos de voto, então não procure playerIdentifier nele.

Evento de teste nunca gera voto nem altera o ranking. É a forma recomendada de validar sua integração antes de anunciar a recompensa aos jogadores: não é preciso pedir para alguém votar de verdade só para ver se o webhook chega.

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 depende do plano do jogo: 24 horas no gratuito, 12 nos pagos. Não prometa recompensa mais frequente que isso — e não deduza o intervalo por conta própria: pergunte ao vote-status, que responde com a data exata do seu jogo.
  • Se suspeitar de vazamento, gere a chave nova e revogue a antiga — são duas ações separadas no painel. Gerar uma nova não desativa a anterior: enquanto você não revogar, a chave vazada continua valendo e continua lendo /api/v1/votes, que devolve identificador de jogador.

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.