# CSGOWTFD Backend API Documentation **Version**: 1.0 **Base URL**: `http://localhost:8000` (default) **Backend Repository**: https://somegit.dev/CSGOWTF/csgowtfd **Framework**: Go + Gin + Ent ORM **Database**: PostgreSQL --- ## Table of Contents - [Overview](#overview) - [API Endpoints](#api-endpoints) - [Player Endpoints](#player-endpoints) - [Match Endpoints](#match-endpoints) - [Matches Listing](#matches-listing) - [Data Models](#data-models) - [Integration Guide](#integration-guide) - [Error Handling](#error-handling) - [CS2 Migration Notes](#cs2-migration-notes) - [Rate Limiting](#rate-limiting) - [Caching Strategy](#caching-strategy) - [Testing](#testing) - [Swagger Documentation](#swagger-documentation) - [Support & Updates](#support--updates) 1. [Overview](#overview) 2. [API Endpoints](#api-endpoints) - [Player Endpoints](#player-endpoints) - [Match Endpoints](#match-endpoints) - [Matches Listing](#matches-listing) - [Sitemap](#sitemap) 3. [Data Models](#data-models) 4. [Integration Guide](#integration-guide) 5. [Error Handling](#error-handling) --- ## Overview The CSGOWTFD backend is a REST API service that provides Counter-Strike match statistics, player profiles, and demo parsing capabilities. It uses: - **Web Framework**: Gin (Go) - **ORM**: Ent (Entity Framework for Go) - **Database**: PostgreSQL with pgx driver - **Cache**: Redis - **Steam Integration**: Steam API + demo parsing ### Configuration Default backend configuration: ```yaml httpd: listen: ':8000' cors_allow_domains: ['*'] database: driver: 'pgx' connection: 'postgres://username:password@localhost:5432/database_name' redis: addr: 'localhost:6379' ``` ### CORS The backend supports CORS with configurable allowed domains. By default, all origins are allowed in development. --- ## API Endpoints For detailed documentation on the matches API specifically, see [MATCHES_API.md](MATCHES_API.md). ## API Endpoints ### Player Endpoints #### 1. Get Player Profile Retrieves comprehensive player statistics and match history. **Endpoint**: `GET /player/:id` **Alternative**: `GET /player/:id/next/:time` **Parameters**: - `id` (path, required): Steam ID (uint64) - `time` (path, optional): Unix timestamp for pagination (get matches before this time) **Response** (200 OK): ```json { "id": 76561198012345678, "name": "PlayerName", "avatar": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/...", "vanity_url": "custom-url", "vanity_url_real": "custom-url", "steam_updated": "2024-11-04T10:30:00Z", // Optional: may not always be provided "profile_created": "2015-03-12T00:00:00Z", "wins": 1250, "looses": 980, "ties": 45, "vac_count": 0, "vac_date": null, "game_ban_count": 0, "game_ban_date": null, "oldest_sharecode_seen": "CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", "matches": [ { "match_id": 3589487716842078322, "map": "de_inferno", "date": "2024-11-01T18:45:00Z", "score_team_a": 13, "score_team_b": 10, "duration": 2456, "match_result": 1, "max_rounds": 24, "demo_parsed": true, "vac_present": false, "gameban_present": false, "tick_rate": 64.0, // Optional: not always provided by API "stats": { "team_id": 2, "kills": 24, "deaths": 18, "assists": 6, "headshot": 12, "mvp": 3, "score": 56, "kast": 78, // Optional: not always provided by API "rank_old": 18500, "rank_new": 18650, "dmg_enemy": 2450, "dmg_team": 120, "flash_assists": 4, "flash_duration_enemy": 15.6, "flash_total_enemy": 8, "ud_he": 450, "ud_flames": 230, "ud_flash": 5, "ud_smoke": 3, "avg_ping": 25.5, "color": "yellow" } } ] } ``` **Use Case**: Display player profile page with career stats and recent matches. --- #### 2. Get Player Metadata Retrieves lightweight player metadata (recent matches summary). **Endpoint**: `GET /player/:id/meta` **Alternative**: `GET /player/:id/meta/:limit` **Parameters**: - `id` (path, required): Steam ID - `limit` (path, optional): Number of recent matches to include (default: 10) **Response** (200 OK): ```json { "id": 76561198012345678, "name": "PlayerName", "avatar": "...", "recent_matches": 25, "last_match_date": "2024-11-01T18:45:00Z", "avg_kills": 21.3, "avg_deaths": 17.8, "avg_kast": 75.2, "win_rate": 56.5 } ``` **Use Case**: Quick player card/preview without full match data. --- #### 3. Track Player Adds a player to the tracking system for automatic match updates. **Endpoint**: `POST /player/:id/track` **Parameters**: - `id` (path, required): Steam ID **Request Body**: ```json { "auth_code": "XXXX-XXXXX-XXXX" } ``` **Response** (200 OK): ```json { "success": true, "message": "Player added to tracking queue" } ``` **Use Case**: Allow users to track their own profile for automatic updates. --- #### 4. Untrack Player Removes a player from the tracking system. **Endpoint**: `DELETE /player/:id/track` **Parameters**: - `id` (path, required): Steam ID **Response** (200 OK): ```json { "success": true, "message": "Player removed from tracking" } ``` --- ### Match Endpoints #### 5. Parse Match from Share Code Triggers parsing of a CS:GO/CS2 match from a share code. **Endpoint**: `GET /match/parse/:sharecode` **Parameters**: - `sharecode` (path, required): CS:GO match share code (e.g., `CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX`) **Response** (200 OK): ```json { "match_id": 3589487716842078322, "status": "parsing", "message": "Demo download and parsing initiated" } ``` **Response** (202 Accepted): ```json { "match_id": 3589487716842078322, "status": "queued", "estimated_time": 120 } ``` **Use Case**: User submits match share code for analysis. --- #### 6. Get Match Details Retrieves full match details including all player statistics. **Endpoint**: `GET /match/:id` **Parameters**: - `id` (path, required): Match ID (uint64) **Response** (200 OK): ```json { "match_id": 3589487716842078322, "share_code": "CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX", "map": "de_inferno", "date": "2024-11-01T18:45:00Z", "score_team_a": 13, "score_team_b": 10, "duration": 2456, "match_result": 1, "max_rounds": 24, "demo_parsed": true, "vac_present": false, "gameban_present": false, "tick_rate": 64.0, "players": [ { "id": 76561198012345678, "name": "Player1", "avatar": "...", "team_id": 2, "kills": 24, "deaths": 18, "assists": 6, "headshot": 12, "mvp": 3, "score": 56, "kast": 78, "rank_old": 18500, "rank_new": 18650, "dmg_enemy": 2450, "dmg_team": 120, "flash_assists": 4, "flash_duration_enemy": 15.6, "flash_duration_team": 2.3, "flash_duration_self": 1.2, "flash_total_enemy": 8, "flash_total_team": 1, "flash_total_self": 1, "ud_he": 450, "ud_flames": 230, "ud_flash": 5, "ud_smoke": 3, "ud_decoy": 0, "mk_2": 4, "mk_3": 2, "mk_4": 1, "mk_5": 0, "avg_ping": 25.5, "color": "yellow", "crosshair": "CSGO_XXXXXXXXXXXX" } ] } ``` **Use Case**: Display match overview page with scoreboard. --- #### 7. Get Match Weapons Retrieves weapon statistics for all players in a match. **Endpoint**: `GET /match/:id/weapons` **Parameters**: - `id` (path, required): Match ID **Response** (200 OK): ```json { "match_id": 3589487716842078322, "weapons": [ { "player_id": 76561198012345678, "weapon_stats": [ { "eq_type": 7, "weapon_name": "AK-47", "kills": 12, "damage": 1450, "hits": 48, "hit_groups": { "head": 8, "chest": 25, "stomach": 8, "left_arm": 3, "right_arm": 2, "left_leg": 1, "right_leg": 1 } } ] } ] } ``` **Use Case**: Display detailed weapon performance page for match. --- #### 8. Get Match Rounds Retrieves round-by-round statistics for a match. **Endpoint**: `GET /match/:id/rounds` **Parameters**: - `id` (path, required): Match ID **Response** (200 OK): ```json { "match_id": 3589487716842078322, "rounds": [ { "round": 1, "winner": 2, "win_reason": "elimination", "players": [ { "player_id": 76561198012345678, "bank": 800, "equipment": 650, "spent": 650, "kills_in_round": 2, "damage_in_round": 234 } ] } ] } ``` **Use Case**: Display economy tab with round-by-round breakdown. --- #### 9. Get Match Chat Retrieves in-game chat messages from a match. **Endpoint**: `GET /match/:id/chat` **Parameters**: - `id` (path, required): Match ID **Response** (200 OK): ```json { "match_id": 3589487716842078322, "messages": [ { "player_id": 76561198012345678, "player_name": "Player1", "message": "nice shot!", "tick": 15840, "round": 8, "all_chat": true, "timestamp": "2024-11-01T19:12:34Z" } ] } ``` **Use Case**: Display chat log page for match. --- ### Matches Listing #### 10. Get Matches List Retrieves a paginated list of matches. **Endpoint**: `GET /matches` **Alternative**: `GET /matches/next/:time` **Parameters**: - `time` (path, optional): Unix timestamp for pagination (use with `/matches/next/:time`) - Query parameters: - `limit` (optional): Number of matches to return (default: 50, max: 100) - `map` (optional): Filter by map name (e.g., `de_inferno`) - `player_id` (optional): Filter by player Steam ID **Response** (200 OK): **IMPORTANT**: This endpoint returns a **plain array**, not an object with properties. ```json [ { "match_id": "3589487716842078322", "map": "de_inferno", "date": 1730487900, "score": [13, 10], "duration": 2456, "match_result": 1, "max_rounds": 24, "parsed": true, "vac": false, "game_ban": false } ] ``` **Field Descriptions**: - `match_id`: Unique match identifier (uint64 as string) - `map`: Map name (can be empty string if not parsed) - `date`: Unix timestamp (seconds since epoch) - `score`: Array with two elements `[team_a_score, team_b_score]` - `duration`: Match duration in seconds - `match_result`: 0 = tie, 1 = team_a win, 2 = team_b win - `max_rounds`: Maximum rounds (24 for MR12, 30 for MR15) - `parsed`: Whether the demo has been parsed - `vac`: Whether any player has a VAC ban - `game_ban`: Whether any player has a game ban **Pagination**: - To get the next page, use the `date` field from the last match in the array - Request `/matches/next/{timestamp}` where `{timestamp}` is the Unix timestamp - Continue until the response returns fewer matches than requested **Use Case**: Display matches listing page with filters. --- ### Sitemap #### 11. Get Sitemap Index Returns XML sitemap index for SEO. **Endpoint**: `GET /sitemap.xml` **Response** (200 OK): ```xml https://csgow.tf/sitemap/1 2024-11-04 ``` --- #### 12. Get Sitemap Pages Returns XML sitemap for specific page range. **Endpoint**: `GET /sitemap/:id` **Parameters**: - `id` (path, required): Sitemap page number **Response** (200 OK): ```xml https://csgow.tf/match/3589487716842078322 2024-11-01 never 0.8 ``` --- ## Data Models ### Match ```typescript interface Match { match_id: number; // uint64, unique identifier share_code: string; // CS:GO share code map: string; // Map name (e.g., "de_inferno") date: string; // ISO 8601 timestamp score_team_a: number; // Final score for team A (T/CT) score_team_b: number; // Final score for team B (CT/T) duration: number; // Match duration in seconds match_result: number; // 0 = tie, 1 = team_a win, 2 = team_b win max_rounds: number; // Max rounds (24 for MR12, 30 for MR15) demo_parsed: boolean; // Demo parsing status vac_present: boolean; // Any VAC bans in match gameban_present: boolean; // Any game bans in match tick_rate: number; // Server tick rate (64 or 128) players?: MatchPlayer[]; // Array of player stats } ``` ### Player ```typescript interface Player { id: number; // uint64, Steam ID name: string; // Steam display name avatar: string; // Avatar URL vanity_url?: string; // Custom Steam URL vanity_url_real?: string; // Actual vanity URL steam_updated: string; // Last Steam profile update profile_created?: string; // Steam account creation date wins?: number; // Total competitive wins looses?: number; // Total competitive losses (note: typo in backend) ties?: number; // Total ties vac_count?: number; // Number of VAC bans vac_date?: string; // Date of last VAC ban game_ban_count?: number; // Number of game bans game_ban_date?: string; // Date of last game ban oldest_sharecode_seen?: string; // Oldest match on record matches?: Match[]; // Recent matches } ``` ### MatchPlayer ```typescript interface MatchPlayer { match_stats: number; // Match ID reference player_stats: number; // Player ID reference team_id: number; // 2 = T, 3 = CT // Performance kills: number; deaths: number; assists: number; headshot: number; // Headshot kills mvp: number; // MVP stars score: number; // In-game score kast: number; // KAST percentage (0-100) // Rank rank_old?: number; // Rating before match (CS2: 0-30000) rank_new?: number; // Rating after match // Damage dmg_enemy?: number; // Damage to enemies dmg_team?: number; // Team damage // Multi-kills mk_2?: number; // Double kills mk_3?: number; // Triple kills mk_4?: number; // Quad kills mk_5?: number; // Ace (5k) // Utility damage ud_he?: number; // HE grenade damage ud_flames?: number; // Molotov/Incendiary damage ud_flash?: number; // Flash grenades used ud_smoke?: number; // Smoke grenades used ud_decoy?: number; // Decoy grenades used // Flash statistics flash_assists?: number; // Assists from flashes flash_duration_enemy?: number; // Total enemy blind time flash_duration_team?: number; // Total team blind time flash_duration_self?: number; // Self-flash time flash_total_enemy?: number; // Enemies flashed count flash_total_team?: number; // Teammates flashed count flash_total_self?: number; // Self-flash count // Other crosshair?: string; // Crosshair settings color?: 'green' | 'yellow' | 'purple' | 'blue' | 'orange' | 'grey'; avg_ping?: number; // Average ping } ``` ### RoundStats ```typescript interface RoundStats { round: number; // Round number (1-24 for MR12) bank: number; // Money at round start equipment: number; // Equipment value purchased spent: number; // Total money spent match_player_id: number; // Reference to MatchPlayer } ``` ### Weapon ```typescript interface Weapon { victim: number; // Player ID who was hit/killed dmg: number; // Damage dealt eq_type: number; // Weapon type ID hit_group: number; // Hit location (1=head, 2=chest, etc.) match_player_id: number; // Reference to MatchPlayer } ``` ### Message ```typescript interface Message { message: string; // Chat message content all_chat: boolean; // true = all chat, false = team chat tick: number; // Game tick when sent match_player_id: number; // Reference to MatchPlayer } ``` --- ## Integration Guide ### Frontend Setup 1. **Install HTTP Client** ```bash npm install axios # or use native fetch ``` 2. **Create API Client** (`src/lib/api/client.ts`) ```typescript import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; const API_TIMEOUT = import.meta.env.VITE_API_TIMEOUT || 10000; class APIClient { private client: AxiosInstance; constructor() { this.client = axios.create({ baseURL: API_BASE_URL, timeout: API_TIMEOUT, headers: { 'Content-Type': 'application/json' } }); // Response interceptor for error handling this.client.interceptors.response.use( (response) => response, (error) => { if (error.response) { // Server responded with error console.error('API Error:', error.response.status, error.response.data); } else if (error.request) { // Request made but no response console.error('Network Error:', error.message); } return Promise.reject(error); } ); } async get(url: string, config?: AxiosRequestConfig): Promise { const response = await this.client.get(url, config); return response.data; } async post(url: string, data?: any, config?: AxiosRequestConfig): Promise { const response = await this.client.post(url, data, config); return response.data; } async delete(url: string, config?: AxiosRequestConfig): Promise { const response = await this.client.delete(url, config); return response.data; } } export const apiClient = new APIClient(); ``` 3. **Create API Endpoints** (`src/lib/api/players.ts`) ```typescript import { apiClient } from './client'; import type { Player, Match } from '$lib/types'; export const playersAPI = { async getPlayer(steamId: string | number, beforeTime?: number): Promise { const url = beforeTime ? `/player/${steamId}/next/${beforeTime}` : `/player/${steamId}`; return apiClient.get(url); }, async getPlayerMeta(steamId: string | number, limit: number = 10) { return apiClient.get(`/player/${steamId}/meta/${limit}`); }, async trackPlayer(steamId: string | number, authCode: string) { return apiClient.post(`/player/${steamId}/track`, { auth_code: authCode }); }, async untrackPlayer(steamId: string | number) { return apiClient.delete(`/player/${steamId}/track`); } }; ``` 4. **Create API Endpoints** (`src/lib/api/matches.ts`) ```typescript import { apiClient } from './client'; import type { Match } from '$lib/types'; export const matchesAPI = { async getMatch(matchId: string | number): Promise { return apiClient.get(`/match/${matchId}`); }, async getMatchWeapons(matchId: string | number) { return apiClient.get(`/match/${matchId}/weapons`); }, async getMatchRounds(matchId: string | number) { return apiClient.get(`/match/${matchId}/rounds`); }, async getMatchChat(matchId: string | number) { return apiClient.get(`/match/${matchId}/chat`); }, async parseMatch(shareCode: string) { return apiClient.get(`/match/parse/${shareCode}`); }, async getMatches(params?: { limit?: number; before_time?: number; map?: string; player_id?: string; }) { const url = params?.before_time ? `/matches/next/${params.before_time}` : '/matches'; const limit = params?.limit || 50; // API returns a plain array, not an object const data = await apiClient.get(url, { params: { limit: limit + 1, // Request one extra to check if there are more map: params?.map, player_id: params?.player_id } }); // Check if there are more matches const hasMore = data.length > limit; const matches = hasMore ? data.slice(0, limit) : data; // Get timestamp for next page const lastMatch = matches[matches.length - 1]; const nextPageTime = hasMore && lastMatch ? Math.floor(new Date(lastMatch.date).getTime() / 1000) : undefined; // Transform to new format return transformMatchesListResponse(matches, hasMore, nextPageTime); } }; ``` 5. **Use in SvelteKit Routes** (`src/routes/player/[id]/+page.ts`) ```typescript import { playersAPI } from '$lib/api/players'; import { error } from '@sveltejs/kit'; import type { PageLoad } from './$types'; export const load: PageLoad = async ({ params }) => { try { const player = await playersAPI.getPlayer(params.id); return { player }; } catch (err) { throw error(404, 'Player not found'); } }; ``` --- ## Error Handling ### HTTP Status Codes | Status | Meaning | Common Causes | | ------ | --------------------- | --------------------------------------- | | 200 | Success | Request completed successfully | | 202 | Accepted | Request queued (async operations) | | 400 | Bad Request | Invalid parameters or malformed request | | 404 | Not Found | Player/match doesn't exist | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Internal Server Error | Backend issue | | 503 | Service Unavailable | Backend down or maintenance | ### Error Response Format ```json { "error": "Match not found", "code": "MATCH_NOT_FOUND", "details": { "match_id": 3589487716842078322 } } ``` ### Retry Strategy For transient errors (500, 503), implement exponential backoff: ```typescript async function retryRequest( fn: () => Promise, maxRetries: number = 3, baseDelay: number = 1000 ): Promise { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; const delay = baseDelay * Math.pow(2, i); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw new Error('Max retries exceeded'); } ``` --- ## CS2 Migration Notes ### Rank System Changes **CS:GO** used 18 ranks (Silver I to Global Elite): - Values: 0-18 **CS2** uses Premier Rating: - Values: 0-30,000 - No traditional ranks in Premier mode **Backend Compatibility**: - `rank_old` and `rank_new` fields now store Premier rating (0-30000) - Frontend must detect game version and display accordingly ### MR12 vs MR15 **CS:GO**: MR15 (max 30 rounds) **CS2**: MR12 (max 24 rounds) Check `max_rounds` field in Match data: - `24` = MR12 (CS2) - `30` = MR15 (CS:GO) ### Game Mode Detection To determine if a match is CS:GO or CS2: 1. Check `date` field (CS2 released Sept 2023) 2. Check `max_rounds` (24 = likely CS2) 3. Backend may add `game_version` field in future updates --- ## Rate Limiting **Current Limits** (may vary by deployment): - **Steam API**: 1 request per second (backend handles this) - **Demo Parsing**: Max 6 concurrent parses - **Frontend API**: No explicit limit, but use reasonable request rates **Best Practices**: - Implement client-side caching (5-15 minutes for match data) - Use debouncing for search inputs (300ms) - Batch requests when possible --- ## Caching Strategy ### Backend Caching (Redis) The backend uses Redis for: - Steam API responses (7 days) - Match data (permanent until invalidated) - Player profiles (7 days) ### Frontend Caching Recommendations ```typescript // In-memory cache with TTL class DataCache { private cache = new Map(); set(key: string, data: T, ttlMs: number) { this.cache.set(key, { data, expires: Date.now() + ttlMs }); } get(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expires) { this.cache.delete(key); return null; } return entry.data; } } // Usage const matchCache = new DataCache(); const cachedMatch = matchCache.get(`match:${matchId}`); if (!cachedMatch) { const match = await matchesAPI.getMatch(matchId); matchCache.set(`match:${matchId}`, match, 15 * 60 * 1000); // 15 min } ``` --- ## Testing ### Mock Data Use MSW (Mock Service Worker) for testing: ```typescript // src/mocks/handlers/matches.ts import { http, HttpResponse } from 'msw'; export const matchHandlers = [ http.get('/match/:id', ({ params }) => { return HttpResponse.json({ match_id: parseInt(params.id as string), map: 'de_inferno', date: '2024-11-01T18:45:00Z', score_team_a: 13, score_team_b: 10 // ... rest of mock data }); }) ]; ``` --- ## Swagger Documentation The backend provides interactive API documentation at: **URL**: `{API_BASE_URL}/api/swagger` This Swagger UI allows you to: - Explore all endpoints - Test API calls directly - View request/response schemas - See authentication requirements --- ## Support & Updates - **Backend Issues**: https://somegit.dev/CSGOWTF/csgowtfd/issues - **Frontend Issues**: https://somegit.dev/CSGOWTF/csgowtf/issues - **API Changes**: Watch the backend repository for updates --- **Document Version**: 1.0 **Last Updated**: 2025-11-04 **Maintained By**: CSGOW.TF Development Team