Products Docs Help Store Discord

Web API Integration

Real-time leaderboards, battle replays, and usage statistics for your website.

Build competitive Pokemon dashboards with CobbleRanked’s HTTP API. Push ranked battle data to your web server in real-time.

This page covers the developer-facing payload schemas and server implementation examples. For admin-facing setup, see Web API Configuration.

Features

  • Live Leaderboards - Top players by ELO
  • Battle Replays - Full Showdown protocol logs
  • Match Results - Per-match history with team details and ELO changes
  • Usage Statistics - Pokemon usage with win rates, banded by ELO tier
  • Configurable - Toggle each data type and control sync frequency

How It Works

CobbleRanked is an HTTP client, not a server. It pushes JSON POST requests to endpoints you define on your own web server. There is no port, host, or CORS to configure on the CobbleRanked side — your web server is what receives the data.

On cross-server setups, only the battle server pushes data (crossServer.battleServerOnly: true by default).


Endpoints

CobbleRanked posts to these paths under your configured endpoint.baseUrl:

Path (default)DataTrigger
/api/usage-statsPokemon usage with win rates, banded by ELO tierPeriodic (sync.intervalMinutes, default 60m)
/api/leaderboardTop N players by ELO per formatPeriodic
/api/battle-replayFull Showdown battle log + team speciesAfter each ranked battle
/api/match-resultPer-match ELO change + full team compositionAfter each ranked battle

Toggle each one with sync.dataTypes.* in api.yaml.


Configuration

File: config/cobbleranked/api.yaml

enabled: false

endpoint:
  baseUrl: "https://your-server.com"
  paths:
    pushUsageStats: "/api/usage-stats"
    pushLeaderboard: "/api/leaderboard"
    pushBattleReplay: "/api/battle-replay"
    pushMatchResult: "/api/match-result"

auth:
  apiKey: "your-secret-key"   # sent as the X-API-Key header
  headers: {}                 # extra headers, if your API needs them

sync:
  intervalMinutes: 60
  leaderboardLimit: 100       # max players per format (0 or negative = unlimited)
  dataTypes:
    usageStats: true
    leaderboard: true
    battleReplays: false
    matchResults: false
    eloTiers:                 # ELO buckets for usage stats
      - { minElo: 0 }
      - { minElo: 1300 }
      - { minElo: 1500 }
      - { minElo: 1700 }
  push:
    enabled: true
    batchIntervalSeconds: 30
    onlyIfChanged: true       # hash-based dedupe
  pull:
    enabled: false

http:
  timeoutSeconds: 30
  retryAttempts: 3
  retryDelaySeconds: 5

crossServer:
  battleServerOnly: true      # only the battle server pushes data
Server Authentication

CobbleRanked sends X-API-Key header with your configured key. Verify this on your server:

const VALID_KEY = process.env.API_KEY;

app.use((req, res, next) => {
  const receivedKey = req.headers['x-api-key'];
  if (receivedKey !== VALID_KEY) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

Data Payloads

All payloads are JSON POST bodies. Field names are stable and match the Kotlin models in ApiModels.kt.

Leaderboard

{
  "serverId": "battle-server-1",
  "seasonName": "Season 1",
  "timestamp": "2026-01-02T12:00:00Z",
  "formats": {
    "SINGLES": {
      "format": "SINGLES",
      "players": [
        {
          "rank": 1,
          "uuid": "550e8400-...",
          "playerName": "ChampionPlayer",
          "elo": 2150,
          "tier": "CHERISH",
          "wins": 245,
          "losses": 32,
          "matches": 277,
          "winRate": 88.45,
          "currentStreak": 5,
          "bestStreak": 18
        }
      ]
    }
  }
}

Match Result

Per-match data with ELO change and full team composition:

{
  "matchId": "abc-123",
  "serverId": "battle-server-1",
  "seasonName": "Season 1",
  "format": "SINGLES",
  "matchType": "RANKED",
  "timestamp": "2026-01-02T12:00:00Z",
  "durationSeconds": 540,
  "endReason": "VICTORY",
  "players": [
    {
      "uuid": "550e8400-...",
      "playerName": "PlayerA",
      "eloBefore": 1500,
      "eloAfter": 1524,
      "eloChange": 24,
      "isWinner": true,
      "faintedCount": 1,
      "team": [
        {
          "species": "Garchomp",
          "ability": "Rough Skin",
          "item": "Choice Scarf",
          "moves": ["Earthquake", "Outrage", "Stone Edge", "Fire Fang"],
          "nature": "Jolly",
          "evSpread": "4/252/0/0/0/252"
        }
      ]
    }
  ]
}

Battle Replay

Full Showdown protocol log pushed after each ranked battle ends:

{
  "matchId": "abc-123",
  "serverId": "battle-server-1",
  "seasonName": "Season 1",
  "format": "SINGLES",
  "timestamp": "2026-01-02T12:00:00Z",
  "turnCount": 24,
  "endReason": "VICTORY",
  "players": [
    {
      "uuid": "550e8400-...",
      "playerName": "PlayerA",
      "team": ["Garchomp", "Rotom-Wash", "Togekiss"],
      "isWinner": true
    }
  ],
  "battleLog": [
    "|start|",
    "|switch|p1a: Garchomp|Garchomp, L100|100/100",
    "|turn|1"
  ]
}

Usage Statistics

Pokemon usage with win rates, banded by the ELO tiers you configured:

{
  "serverId": "battle-server-1",
  "seasonName": "Season 1",
  "timestamp": "2026-01-02T12:00:00Z",
  "formats": {
    "SINGLES": {
      "1500+": {
        "minElo": 1500,
        "maxElo": null,
        "totalBattles": 1250,
        "totalPokemon": 3750,
        "species": [
          {
            "name": "Garchomp",
            "usagePercent": 45.2,
            "count": 562,
            "winRate": 62.3,
            "abilities": { "Rough Skin": 85.3 },
            "items": { "Choice Scarf": 42.1 },
            "moves": { "Earthquake": 98.2 },
            "natures": { "Jolly": 70.1 },
            "evSpreads": { "4/252/0/0/0/252": 65.0 },
            "teammates": { "Rotom-Wash": 30.5 }
          }
        ]
      }
    }
  }
}

ELO-Banded Aggregation

Usage statistics are split by ELO band, not pooled into one global average. You configure the band boundaries in api.yaml:

sync:
  eloTiers:
    - { minElo: 0 }
    - { minElo: 1300 }
    - { minElo: 1500 }
    - { minElo: 1700 }

Each band becomes a key in the payload (e.g. 0+, 1300+, 1500+, 1700+) with its own totalBattles and per-species stats. If Garchomp sits at 45% usage overall but 70% in the 1700+ band, your dashboard can show that the high-ELO metagame leans heavily on it — useful for “meta by rating” views and counter-builder tools.

Teammate Analysis

Every species entry carries a teammates map: for each Pokémon, how often each other species appears alongside it on the same team. Use it to surface popular cores and duos:

  • Garchomp → { Rotom-Wash: 30.5%, Togekiss: 18.2% } → “Garchomp is most often paired with Rotom-Wash”
  • Combine with abilities, items, moves, and natures/evSpreads to reconstruct common builds and suggest counters.

Use Cases

You want to build…Use this dataHow
Live leaderboard widget on your homepage/api/leaderboardRender top-N per format; refresh on the sync.intervalMinutes cadence
Metagame / usage dashboard/api/usage-statsSlice by ELO band to show “meta at my rating”; rank species by usagePercent and winRate
Team builder / counter-picker/api/usage-stats (abilities, items, moves, natures, evSpreads, teammates)Show the most common build for each species and its frequent partners; suggest counters by win-rate inversion
Player profile / match history/api/match-resultList a player’s recent matches with ELO before→after, team composition, and outcome
Battle replay viewer/api/battle-replayFeed the battleLog (Showdown protocol) into any Showdown replay renderer

📝 All payloads are push-only (CobbleRanked → your server). For a player-profile view, store incoming match-result posts in your own database and query by uuid.

Example output

A minimal community site consuming these endpoints — a rating/tier view and per-format win statistics, both rendered from /api/leaderboard and /api/usage-stats:

Leaderboard showing per-format win statistics


Server Implementation

Express.js Example:

const express = require('express');
const app = express();

app.use(express.json());

// Leaderboard endpoint
app.post('/api/leaderboard', (req, res) => {
  const { seasonName, formats } = req.body;
  console.log(`Leaderboard for ${seasonName}:`, Object.keys(formats));
  res.json({ success: true });
});

// Match result endpoint
app.post('/api/match-result', (req, res) => {
  const { players } = req.body;
  console.log(`Match result: ${players[0].playerName} vs ${players[1].playerName}`);
  res.json({ success: true });
});

// Battle replay endpoint
app.post('/api/battle-replay', (req, res) => {
  const { battleLog, turnCount } = req.body;
  console.log(`Replay: ${turnCount} turns, ${battleLog.length} log lines`);
  res.json({ success: true });
});

// Usage stats endpoint
app.post('/api/usage-stats', (req, res) => {
  const { formats } = req.body;
  console.log(`Usage stats received for ${Object.keys(formats).length} format(s)`);
  res.json({ success: true });
});

app.listen(3000);
NestJS Example (TypeScript)
import { Controller, Post, Body } from '@nestjs/common';

class LeaderboardPayload {
  serverId: string;
  seasonName: string;
  formats: Record<string, {
    players: Array<{
      uuid: string;
      playerName: string;
      elo: number;
    }>;
  }>;
}

@Controller('api')
export class ApiController {
  @Post('leaderboard')
  async leaderboard(@Body() payload: LeaderboardPayload) {
    // Process leaderboard data
    return { success: true };
  }
}

Testing

Curl Commands:

# Test leaderboard
curl -X POST https://your-server.com/api/leaderboard \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{"serverId":"test","seasonName":"Season 1","formats":{}}'

# Test match result
curl -X POST https://your-server.com/api/match-result \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-secret-key" \
  -d '{"matchId":"test","players":[]}'

You can also trigger a sync manually in-game: /rankedadmin api sync (push), /rankedadmin api status (state), /rankedadmin api test (config sanity check).


Security

⚠️ Production Requirements:

  • Use HTTPS only
  • Validate X-API-Key header
  • Rate limit requests (100 req/min recommended)
  • Sanitize all input data
  • Use read-only database accounts for queries
-- Create read-only database user (if your web app reads the CobbleRanked DB directly)
CREATE USER 'cobbleranked_web'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT ON cobbleranked.* TO 'cobbleranked_web'@'%';

See Also