API pública do Gamerfy

English summary (for the pubg.report team). Base URL: https://api.gamerfy.gg. Create an app in the desktop app (Configurações → Desenvolvedor) to get a client_id and a client_secret (shown once). POST /oauth2/token with grant_type=client_credentials returns a gfa_… bearer token valid for 60 days; send it as Authorization: Bearer …. Four endpoints, all Twitch-shaped ({ data: [...], pagination: { cursor } }, snake_case, ISO 8601 UTC dates): GET /v1/users (by id or login), GET /v1/streams (personal lives on air), GET /v1/videos (replays, type=archive) and GET /v1/games. Every replay has a public page: https://gamerfy.gg/videos/{id}?t=1h2m3s (same t as Twitch, plain seconds also accepted). To link a match event to the replay: offset = event_time_utc − video.created_at, with no lead correction — created_at is the wall clock of the first frame of the recording, millisecond precision. Limits: 800 requests per minute per app, up to 100 ids per call. Replays are kept for 14 days.

Este documento é o guia da API pública: como criar um app, obter o token e consultar usuários, lives, replays e jogos. O primeiro consumidor é o pubg.report, que hoje cruza a telemetria das partidas de PUBG com os VODs da Twitch; a API foi desenhada para que o adaptador dele seja quase o mesmo. A spec da rodada está em docs/superpowers/specs/2026-09-08-replays-public-api-apps-design.md.

1. Endereços

Ambiente Base Observação
Produção e test https://api.gamerfy.gg vhost do Caddy que reescreve /{uri} para /api/public{uri} no backend
Desenvolvimento local http://localhost:4500/api/public o mesmo backend, sem o vhost

Os exemplos abaixo usam a base de produção. Em dev, troque a base e mantenha o resto do caminho.

2. Criar um app

  1. No app desktop, abra Configurações → Desenvolvedor.
  2. Clique em Criar app e dê um nome (até 60 caracteres).
  3. Copie o client secret na hora. Ele é mostrado uma única vez; o backend guarda só o SHA-256. Se perder, use Gerar novo segredo: o anterior deixa de valer e todos os tokens emitidos com ele são apagados.
  4. O client id fica visível na lista e pode ser copiado quando quiser.
Credencial Formato Onde vive
client_id 30 caracteres [a-z0-9] público; pode ir em arquivo de configuração
client_secret gfs_ + 40 caracteres [A-Za-z0-9] segredo; só no servidor de quem integra
token de app gfa_ + 40 caracteres [A-Za-z0-9] segredo; expira em 60 dias

Cada conta pode ter até 10 apps. Apagar o app apaga o segredo e todos os tokens. Os apps pertencem a um usuário do Gamerfy; não há OAuth de usuário nesta versão (ver §9).

3. Obter o token

POST /oauth2/token com grant_type=client_credentials. O corpo pode ser JSON ou application/x-www-form-urlencoded (o formato que a Twitch aceita).

JSON:

curl -X POST https://api.gamerfy.gg/oauth2/token \
  -H 'Content-Type: application/json' \
  -d '{"client_id":"abc123…","client_secret":"gfs_…","grant_type":"client_credentials"}'

Form-urlencoded:

curl -X POST https://api.gamerfy.gg/oauth2/token \
  -d 'client_id=abc123…' \
  -d 'client_secret=gfs_…' \
  -d 'grant_type=client_credentials'

Resposta:

{
  "access_token": "gfa_4kQ9…",
  "expires_in": 5184000,
  "token_type": "bearer"
}

expires_in é em segundos (60 dias). O token é opaco: não há nada para decodificar dentro dele. Guarde e reutilize até expirar; pedir um token novo a cada chamada é desperdício e conta no limite.

Regras:

4. Validar o token

GET /oauth2/validate diz de quem é o token e quanto tempo falta.

curl https://api.gamerfy.gg/oauth2/validate \
  -H 'Authorization: Bearer gfa_4kQ9…'
{
  "client_id": "abc123…",
  "app_name": "pubg.report",
  "expires_in": 5180000
}

Token ausente, com formato errado, desconhecido ou expirado: 401 invalid_token.

5. Formato geral

Todas as rotas /v1/* exigem Authorization: Bearer gfa_….

6. Endpoints

6.1 GET /v1/users

Usuários por id ou por login (o username do Gamerfy, sem @). Pelo menos um dos dois; os dois juntos somam até 100.

Parâmetro Tipo Observação
id string, repetível id do usuário
login string, repetível username, comparação sem distinguir maiúsculas
curl 'https://api.gamerfy.gg/v1/users?login=henrique&login=fulano' \
  -H 'Authorization: Bearer gfa_4kQ9…'
{
  "data": [
    {
      "id": "1834021894356992001",
      "login": "henrique",
      "display_name": "Henrique",
      "profile_image_url": "https://files.gamerfy.gg/avatars/…",
      "created_at": "2026-09-01T12:00:00.000Z",
      "links": [
        {
          "game_id": "pubg",
          "account_id": "account.c0e530e9b7244b358def282782f893de",
          "account_name": "HenriqueTJ"
        }
      ]
    }
  ]
}

links traz as contas de jogo que o usuário vinculou e deixou visíveis no perfil. Para o PUBG, account_id é o accountId da API oficial da KRAFTON (account.…) e account_name é o nome do jogador na época do vínculo. É o campo que dispensa o pubg.report de adivinhar o jogador pelo nome do canal: o usuário já disse qual conta é a dele. Usuário sem vínculo visível vem com links: []. Um id ou login que não existe simplesmente não aparece em data. A lista sai da conta mais antiga para a mais nova; um id que não é numérico é 400 invalid_query. profile_image_url é null para quem não tem foto.

6.2 GET /v1/streams

Lives pessoais no ar agora. Sem filtro, lista todas, das mais recentes para as mais antigas.

Parâmetro Tipo Observação
user_id string, repetível até 100
user_login string, repetível até 100
game_id string, repetível slug do jogo (pubg, cs2, …)
first int 1 a 100, padrão 20
after string cursor
curl 'https://api.gamerfy.gg/v1/streams?game_id=pubg&first=50' \
  -H 'Authorization: Bearer gfa_4kQ9…'
{
  "data": [
    {
      "id": "1834100000000000042",
      "user_id": "1834021894356992001",
      "user_login": "henrique",
      "user_name": "Henrique",
      "game_id": "pubg",
      "title": "Ranked de terça",
      "viewer_count": 37,
      "started_at": "2026-09-08T21:30:00.123Z",
      "type": "live",
      "recording_id": "1834100000000000042"
    }
  ],
  "pagination": { "cursor": "eyJzdGFydGVkX2F0Ijoi…" }
}

recording_id é o id que a gravação em curso vai ter quando virar replay em /v1/videos: é o mesmo id da stream. Quando a gravação não está ligada (conta sem gravação, ou storage desligada no ambiente), recording_id é null e id é "{user_id}-{epoch em segundos de started_at}".

6.3 GET /v1/videos

Replays das lives pessoais. Só saem os ready, públicos e dentro da retenção (14 dias). Pelo menos um filtro entre id, user_id e game_id; mais de um restringe (E, não OU).

Parâmetro Tipo Observação
id string, repetível até 100
user_id string um só
game_id string slug do jogo
type string archive (padrão) ou all, que hoje significa o mesmo; qualquer outro valor é 400 invalid_query
first int 1 a 100, padrão 20
after string cursor
curl 'https://api.gamerfy.gg/v1/videos?user_id=1834021894356992001&type=archive' \
  -H 'Authorization: Bearer gfa_4kQ9…'
{
  "data": [
    {
      "id": "1834100000000000042",
      "user_id": "1834021894356992001",
      "user_login": "henrique",
      "user_name": "Henrique",
      "title": "Ranked de terça",
      "created_at": "2026-09-08T21:30:00.123Z",
      "published_at": "2026-09-08T23:44:09.870Z",
      "duration": "2h14m9s",
      "duration_seconds": 8049,
      "url": "https://gamerfy.gg/videos/1834100000000000042",
      "game_id": "pubg",
      "type": "archive",
      "viewable": "public",
      "thumbnail_url": null
    }
  ],
  "pagination": { "cursor": "eyJzdGFydGVkX2F0Ijoi…" }
}
Campo O que é
created_at o relógio de parede do primeiro quadro gravado (EXT-X-PROGRAM-DATE-TIME da trilha de vídeo), em UTC com milissegundos. É a âncora do offset (§8)
published_at quando a live terminou (ended_at); é created_at + duration a menos da latência do fechamento
duration no formato da Twitch: 1h2m3s, 12m5s, 45s
duration_seconds a mesma duração em segundos inteiros
url a página pública do replay; aceita ?t= (§8)
viewable sempre public (replays privados não saem na API)
thumbnail_url sempre null nesta versão (dívida)

Ordem: created_at decrescente, desempate por id decrescente. Um replay truncado por falta de disco ainda aparece; a duração é a do que foi gravado.

6.4 GET /v1/games

Os jogos que o Gamerfy conhece, com os ids da Twitch e da Steam para cruzar catálogos. Sem parâmetros, lista todos.

Parâmetro Tipo Observação
id string, repetível slug (pubg)
name string, repetível nome exato
curl 'https://api.gamerfy.gg/v1/games?id=pubg' \
  -H 'Authorization: Bearer gfa_4kQ9…'
{
  "data": [
    {
      "id": "pubg",
      "name": "PUBG: Battlegrounds",
      "twitch_category_id": "493057",
      "steam_app_id": 578080
    }
  ]
}

game_id nas outras rotas é sempre esse id (o slug), nunca o número da Twitch. id e name não distinguem maiúsculas; os dois juntos somam (OU).

7. Paginação

/v1/streams e /v1/videos paginam por keyset. A resposta traz pagination.cursor enquanto houver mais; repita a chamada com os mesmos filtros e after=<cursor>.

curl 'https://api.gamerfy.gg/v1/videos?game_id=pubg&first=100&after=eyJzdGFydGVkX2F0Ijoi…' \
  -H 'Authorization: Bearer gfa_4kQ9…'

O cursor é opaco (base64url de started_at|id): não monte nem edite. Um cursor corrompido ou de outra rota é 400 invalid_cursor. Não existe before: a lista é sempre do mais novo para o mais velho.

Uma página que veio cheia (data.length === first) sempre traz cursor, mesmo quando por acaso é a última: a chamada seguinte devolve data: [] e pagination: {}. Pare quando pagination.cursor faltar ou data vier vazio. /v1/users e /v1/games não paginam e não trazem pagination.

8. Ligar um evento da partida ao replay

É o caso do pubg.report: a telemetria diz que uma morte aconteceu em event_time_utc; a API diz que o replay começou em video.created_at. O instante no vídeo é a diferença, sem nenhuma correção:

offset_s = (event_time_utc − video.created_at) / 1000
url      = video.url + "?t=" + formatar(offset_s)     # 1h2m3s, ou só os segundos

Por que sem recuo: created_at não é a hora em que a live foi anunciada nem a hora do webhook. É o EXT-X-PROGRAM-DATE-TIME do primeiro segmento gravado, o relógio do próprio servidor de mídia no momento do primeiro quadro, com milissegundos. Na Twitch o created_at do VOD chega a ficar dezenas de segundos antes do primeiro quadro, e por isso o pubg.report desconta 10 s. Aqui o desconto seria erro.

O que ainda entra na conta é a latência do encoder (OBS ou o app) até o servidor, normalmente abaixo de 2 s, e o relógio da máquina do jogador, que a telemetria do PUBG não usa (o _D vem do servidor do jogo). Eventos antes de created_at ou depois de created_at + duration_seconds não têm imagem: o replay pode ter começado depois da partida (gravação ligada só quando a stream apareceu) ou ter sido truncado.

A página https://gamerfy.gg/videos/{id}?t=… aceita 1h2m3s, 2m3s, 45s e 45 (segundos). Um t inválido abre o vídeo do começo. Um replay apagado, privado ou expirado responde "esse replay não está mais disponível".

9. O que o pubg.report faz na Twitch e o equivalente aqui

Na Twitch (Helix) No Gamerfy Diferença
POST id.twitch.tv/oauth2/token (client credentials) POST api.gamerfy.gg/oauth2/token mesmo corpo; token gfa_… de 60 dias
GET helix/users?login= GET /v1/users?login= links[] já traz o account_id do PUBG
GET helix/streams?game_id=493057 GET /v1/streams?game_id=pubg game_id é slug; recording_id aponta para o replay futuro
GET helix/videos?user_id=&type=archive GET /v1/videos?user_id=&type=archive created_at é o primeiro quadro, sem recuo; há duration_seconds
GET helix/games?id= GET /v1/games?id=pubg devolve twitch_category_id para cruzar
Player embed ?t=1h2m3s gamerfy.gg/videos/{id}?t=1h2m3s mesmo formato de t; sem embed por iframe ainda
Ratelimit-Limit/Remaining/Reset (800/min) os mesmos três cabeçalhos, 800/min por app janela fixa de 60 s
Clips, EventSub, OAuth de usuário não existem ver §11

10. Erros e limites

Todo erro tem o formato do backend:

{ "error": "invalid_token", "message": "token de app ausente, inválido ou expirado" }

error é o código estável (compare com ele, nunca com a mensagem); message é para humanos, em pt-BR, e pode mudar; details aparece só em erros de validação (invalid_body, invalid_query), como uma lista de { "path": "first", "message": "…" }.

Status error Quando
400 invalid_body corpo de POST /oauth2/token malformado ou com campo faltando
400 invalid_query parâmetro de consulta rejeitado: nenhum filtro, mais de 100 valores, first fora de 1..100, type desconhecido, id não numérico; details diz qual
400 unsupported_grant_type grant_type diferente de client_credentials
400 invalid_cursor after corrompido ou de outra rota
401 invalid_client client_id ou client_secret errados (mesma mensagem nos dois casos)
401 invalid_token bearer ausente, com formato errado, desconhecido ou expirado
404 not_found rota que não existe
413 payload_too_large corpo acima de 64 KB (o vhost corta antes)
429 rate_limited mais de 800 pedidos no minuto
503 (página de manutenção) durante o deploy; Retry-After: 600

Rate limit: 800 pedidos por minuto por app, contados pelo token, em janela fixa. Cada resposta das rotas autenticadas traz:

Cabeçalho Significado
Ratelimit-Limit 800
Ratelimit-Remaining quantos ainda cabem nesta janela
Ratelimit-Reset segundos até a janela virar

Ao receber 429, espere Ratelimit-Reset segundos. Os tokens de um mesmo app compartilham o limite; criar vários apps para multiplicar o limite é o motivo do teto de 10 apps por conta.

Retenção: cada replay fica 14 dias a partir do fim da live. Depois disso some de /v1/videos e a página responde 404. O streamer pode apagar um replay antes do prazo. Não há como pedir para guardar mais tempo nesta versão.

11. O que ainda não existe