Skip to main content
Documentation

Personal API (/api/v1/me/*)

Programmatic access to your own match data and statistics. Useful for custom dashboards, scripted exports, performance trackers, and integration with other tools.

Early access — Pro tier launching soon

Expanded API access is launching as part of an upcoming Pro tier. During early access, contact us to enable it on your account. The basic data export (downloading your own match history as CSVs) is available to everyone and will remain free.

Scope

The Personal API only ever returns your own data. It will never:

  • Return another user's profile, matches, or stats.
  • Expose opponent PII beyond display name and avatar URL.
  • Return per-match opponent breakdowns (use data export for game-by-game CSVs).
  • Provide platform-wide aggregates or leaderboards (those live in the UI at /games/[slug]/stats and /players).

If you need data about another user or the platform as a whole, it's not here — by design.

Authentication

Either auth mode works on every endpoint:

# Bearer key
curl -H "Authorization: Bearer ttl_user_xxxxx" \
  https://tabletopleague.com/api/v1/me/stats

# JWT cookie (e.g., from a logged-in browser session)
curl --cookie cookies.txt \
  https://tabletopleague.com/api/v1/me/stats

See the Authentication guide for minting and key management.

Rate limits

  • 60 requests per hour by default.
  • Pro tier will raise this (exact cap TBD).
  • All responses include X-RateLimit-* headers.
  • 429 includes an X-RateLimit-Reset epoch second — honor it.

Response envelope

{
  "success": true,
  "data": { ... },
  "_meta": {
    "request_id": "55f8c764-b324-42e1-ac9c-fe385057278d",
    "timestamp": "2026-05-19T03:19:19.645Z",
    "terms_url": "/api-terms"
  }
}

_meta.terms_url is included on every response as a machine-readable reminder of the acceptable use terms you agreed to when minting your key.

Endpoint reference

GET /api/v1/me/profile

Returns your profile fields. The PII here is yours — it's not masked.

{
  "success": true,
  "data": {
    "id": "1f2e3d4c-...",
    "username": "yourname",
    "preferred_name": "Your Name",
    "country": "US",
    "timezone": "America/New_York",
    "avatar_url": "https://...",
    "created_at": "2025-08-04T..."
  }
}

GET /api/v1/me/stats

Aggregated career stats across all your matches.

Query params:

| Param | Type | Description | | ------------- | ------------------- | ----------------------------------------------------------- | | game_system | string (optional) | Filter to one game system slug | | since | ISO date (optional) | Only matches on or after this date. Min granularity: 1 day. |

Example:

curl -H "Authorization: Bearer ttl_user_xxxxx" \
  "https://tabletopleague.com/api/v1/me/stats?game_system=unmatched&since=2026-01-01"
{
  "success": true,
  "data": {
    "total_matches": 142,
    "wins": 81,
    "losses": 61,
    "win_rate": 0.57,
    "recent_form": ["W", "W", "L", "W", "W", "L", "W", "W", "W", "L"]
  }
}

GET /api/v1/me/stats/fighters

Win rate broken down per fighter/character you've used.

Same game_system and since query params as above.

{
  "success": true,
  "data": [
    {
      "fighter_slug": "alice",
      "fighter_name": "Alice",
      "matches": 24,
      "wins": 16,
      "win_rate": 0.667
    },
    {
      "fighter_slug": "achilles",
      "fighter_name": "Achilles",
      "matches": 18,
      "wins": 9,
      "win_rate": 0.5
    }
  ]
}

GET /api/v1/me/stats/opponents

Aggregated stats per opponent you've played. Opponent identity is masked — only display name and avatar URL are returned. No email, Discord ID, real name, country, or any other PII.

{
  "success": true,
  "data": [
    {
      "opponent_display_name": "Player",
      "opponent_avatar_url": "https://...",
      "matches": 7,
      "wins": 4,
      "win_rate": 0.571
    }
  ]
}

The mask is intentional: even if you've played someone 50 times, the API doesn't help you build a personal dossier on them. If you want per-match detail for matches you were in (with opponent display name only), use the CSV data export instead.

Key management

| Endpoint | Use | | ---------------------------- | -------------------------------------------------------------------------------------------------- | | GET /api/v1/me/keys | List your active keys (prefix + last_used_at + expires_at only — never the secret) | | POST /api/v1/me/keys | Mint a new key. Body: { "name": "...", "expires_at": "2026-12-31" }. Returns plaintext once. | | DELETE /api/v1/me/keys/:id | Revoke a key by its ID (from the list endpoint). |

Key management endpoints work with JWT cookie auth only — you cannot create or revoke keys using a Bearer key. This is intentional: it prevents a compromised key from minting more keys.

Example: minimal Node script

const API = "https://tabletopleague.com/api/v1/me";
const TOKEN = process.env.TTL_API_KEY!;

async function get(path: string) {
  const res = await fetch(`${API}${path}`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
  if (!res.ok) {
    const err = await res.json();
    throw new Error(`${res.status} ${err.error.code}: ${err.error.message}`);
  }
  return (await res.json()).data;
}

const profile = await get("/profile");
const stats = await get("/stats?game_system=unmatched");
const fighters = await get("/stats/fighters?game_system=unmatched");

console.log(
  `${profile.username}: ${stats.wins}-${stats.losses} (${(stats.win_rate * 100).toFixed(1)}%)`,
);
fighters.forEach((f) =>
  console.log(
    `  ${f.fighter_name}: ${f.wins}/${f.matches} = ${(f.win_rate * 100).toFixed(1)}%`,
  ),
);

Errors

| Code | When | | -------------------- | --------------------------------------------------------------------------------- | | 401 UNAUTHORIZED | Missing/invalid Bearer key or expired session | | 403 FORBIDDEN | Authenticated, but api.personal.read entitlement not granted (i.e. not yet Pro) | | 429 RATE_LIMITED | Over the per-hour cap. Wait until X-RateLimit-Reset. | | 400 BAD_REQUEST | Invalid query param (e.g., since more granular than 1 day) | | 500 INTERNAL_ERROR | Server error. Include _meta.request_id if you report it. |

Interactive reference

Try every endpoint in the live Scalar UI. The raw OpenAPI spec is at /api/openapi.