# 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