Documentação da API
Tudo que seu jogo precisa para receber votos e entregar recompensa automaticamente.
Como a integração funciona
- Você leva o jogador para o link de votação, com o identificador dele.
- O IdleScore autentica a pessoa, valida a elegibilidade e registra o voto.
- Seu endpoint recebe um evento assinado
vote.confirmed. - 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 emdata.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}devolvehasVoteReward.
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
https://idlescore.com/votar/SEU-SLUG?ref=CHAVE_PUBLICA&player=PLAYER_IDSEU-SLUG— o identificador do seu jogo no IdleScoreref— 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:
{
"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/votesdepois 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
X-IdleScore-Signature: sha256=ASSINATURA
X-IdleScore-Timestamp: TIMESTAMP_UNIX
X-IdleScore-Event-Id: evt_123
Idempotency-Key: evt_123:ep_456A 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
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
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
$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
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', 200Retentativas — 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 resposta | O que fazemos |
|---|---|
2xx | Entregue. Fim. |
5xx | Retentamos. |
408 ou 429 | Retentamos. |
| Sem resposta (timeout, queda de rede, DNS) | Retentamos. |
400, 401, 403, 404, 422… | Nunca mais tentamos. |
3xx | Nunca 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:
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 depoisDepois 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
GET https://idlescore.com/api/v1/games/{slug}/vote-status?player=PLAYER_ID
Authorization: Bearer SUA_CHAVE_SECRETA{
"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
GET https://idlescore.com/api/v1/votes?after=CURSOR&limit=50
Authorization: Bearer SUA_CHAVE_SECRETA{
"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
GET https://idlescore.com/api/v1/integration
Authorization: Bearer SUA_CHAVE_SECRETA{
"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
GET https://idlescore.com/api/v1/games/{slug}Sem autenticação. Devolve apenas o que já é público na página do jogo:
{
"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
| Plano | Requisições/min | Endpoints de webhook |
|---|---|---|
| Início | 300 | 2 |
| Impulso | 900 | 2 |
| Ascensão | 1800 | 5 |
| Prestígio | 4500 | 10 |
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ódigo | Significado |
|---|---|
401 | Chave ausente, inválida ou revogada |
403 | A chave não pertence a este jogo, ou o domínio não está verificado |
404 | Jogo não encontrado |
422 | Parâmetros inválidos |
429 | Limite de requisições atingido |
{ "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
voteIdcomo 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.