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.
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, para vincular o voto à sua integraçãoplayer— 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"
}
}Cabeçalhos
X-IdleScore-Signature: sha256=ASSINATURA
X-IdleScore-Timestamp: TIMESTAMP_UNIX
X-IdleScore-Event-Id: evt_123
Idempotency-Key: vote_123Verificaçã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
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
$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
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', 200Retentativas
Se o seu endpoint não responder 2xx, tentamos de novo até 6 vezes:
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 depoisRespostas 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
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.
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.
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 |
|---|---|---|
| Free | 60 | 1 |
| Start | 120 | 2 |
| Pro | 300 | 5 |
| Master | 900 | 20 |
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ó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 é 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.