# API Reference<no value>
# MatchExec API Reference

REST API for the MatchExec match and tournament management system. **Last Updated for 0.7.0 Release**

**Base URL**: All endpoints are relative to the application root (e.g., `http://localhost:3000`).

**Content-Type**: `application/json` for all request/response bodies unless noted otherwise.

---

## Conventions

### Request Format

- Request bodies use `application/json` unless the endpoint accepts file uploads (multipart/form-data).
- Path parameters are shown as `{paramName}`.
- Fields marked **(required)** must be present; all others are optional.

### Response Format

- Successful responses return the resource or `{ "success": true }`.
- Error responses include an `"error"` string describing the problem.
- Timestamps are ISO 8601 strings (e.g., `"2024-12-31T23:59:59.999Z"`).
- Boolean fields from SQLite are converted to native JSON booleans.

---

## Health & Status

### GET /api/health

Check whether the application and database are operational.

**Response**

```json
{
  "status": "healthy",
  "timestamp": "2024-12-31T23:59:59.999Z",
  "services": {
    "database": "up",
    "web": "up"
  }
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Service is healthy |
| 503 | Service unavailable — database connection failed |

---

### GET /api/stats

Get system-wide statistics.

**Response**

```json
{
  "totalMatches": 42,
  "totalTournaments": 5,
  "totalSignups": 180
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### GET /api/version

Get application version information.

**Response**

```json
{
  "version": "1.2.3",
  "branch": "main",
  "commitHash": "abc1234",
  "isDev": false
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### GET /api/db-status

Get database migration and seeding status. Used by the loading screen UI to track startup progress.

**Response**

```json
{
  "ready": true,
  "progress": "Seeding complete",
  "timestamp": 1735689599000
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

## Games

### GET /api/games

List all supported games with mode and map counts.

**Response**

```json
[
  {
    "id": "overwatch-2",
    "name": "Overwatch 2",
    "genre": "Hero Shooter",
    "developer": "Blizzard Entertainment",
    "description": "...",
    "minPlayers": 2,
    "maxPlayers": 12,
    "supportsAllModes": false,
    "iconUrl": "/games/overwatch-2/icon.png",
    "coverUrl": "/games/overwatch-2/cover.jpg",
    "color": "#F99E1A",
    "mapCount": 24,
    "modeCount": 4
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### GET /api/games/{gameId}

Get details for a specific game.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `gameId` | string | Game identifier |

**Response**

Same shape as a single item from `GET /api/games`.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Game not found |

---

### GET /api/games/{gameId}/modes

List all game modes for a game.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `gameId` | string | Game identifier |

**Response**

```json
[
  {
    "id": "competitive",
    "name": "Competitive",
    "description": "Ranked play",
    "team_size": 5,
    "max_players": 10
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Game not found |

---

### GET /api/games/{gameId}/modes/{modeId}

Get a specific game mode.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `gameId` | string | Game identifier |
| `modeId` | string | Mode identifier |

**Response**

Single mode object with all fields from the game's modes.json.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Mode not found in game |

---

### GET /api/games/{gameId}/modes/{modeId}/maps

List all maps available for a specific mode.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `gameId` | string | Game identifier |
| `modeId` | string | Mode identifier |

**Response**

```json
[
  {
    "id": "king-s-row",
    "name": "King's Row",
    "modeId": "escort",
    "imageUrl": "/games/overwatch-2/maps/kings-row.jpg"
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Game or mode not found |

---

### GET /api/games/{gameId}/maps

List all maps for a game across all modes. CS2 and games with `supportsAllModes = true` receive deduplicated results with additional fields.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `gameId` | string | Game identifier |

**Response**

```json
[
  {
    "id": "dust2",
    "name": "Dust II",
    "modeId": "competitive",
    "imageUrl": "/games/cs2/maps/dust2.jpg",
    "location": "Middle East",
    "modeName": "Competitive",
    "modeDescription": "...",
    "supportedModes": "competitive,deathmatch"
  }
]
```

**Notes**

- The `supportedModes` field is only present for CS2 games.
- Maps are deduplicated when `supportsAllModes = true`.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Game not found |

---

## Discord Channels

### GET /api/channels

List all Discord channels registered in the system.

**Response**

```json
[
  {
    "id": "1",
    "discord_channel_id": "123456789012345678",
    "channel_name": "#announcements",
    "channel_type": "text",
    "send_announcements": true,
    "send_reminders": false,
    "send_match_start": true,
    "send_signup_updates": false,
    "send_health_alerts": false,
    "last_name_refresh": "2024-12-31T23:59:59.999Z",
    "created_at": "2024-01-01T00:00:00.000Z",
    "updated_at": "2024-12-31T23:59:59.999Z"
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### POST /api/channels

Register a new Discord channel.

**Request Body**

```json
{
  "discord_channel_id": "123456789012345678",
  "channel_type": "text",
  "send_announcements": false,
  "send_reminders": false,
  "send_match_start": false,
  "send_signup_updates": false,
  "send_health_alerts": false
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `discord_channel_id` | string | Yes | Discord channel snowflake ID |
| `channel_type` | `"text"` \| `"voice"` | Yes | Channel type |
| `send_announcements` | boolean | No | Receive match/tournament announcements |
| `send_reminders` | boolean | No | Receive player reminders |
| `send_match_start` | boolean | No | Receive match start notifications |
| `send_signup_updates` | boolean | No | Receive signup update notifications |
| `send_health_alerts` | boolean | No | Receive health alert notifications |

**Response**

```json
{
  "success": true,
  "id": "2",
  "message": "Channel created successfully"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Channel created |
| 400 | Validation error |
| 409 | Channel already registered |

---

### PUT /api/channels/{channelId}

Update notification settings for a registered channel.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `channelId` | string | Internal channel ID |

**Request Body**

```json
{
  "send_announcements": true,
  "send_reminders": true,
  "send_match_start": false,
  "send_signup_updates": false,
  "send_health_alerts": false
}
```

All fields are optional; omitted fields remain unchanged.

**Response**

```json
{
  "success": true,
  "message": "Channel notification settings updated successfully"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 400 | Voice channels cannot have notification settings modified manually |
| 404 | Channel not found |

---

### DELETE /api/channels/{channelId}

Remove a channel registration.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `channelId` | string | Internal channel ID |

**Response**

```json
{ "success": true, "message": "Channel deleted successfully" }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Deleted |
| 404 | Channel not found |

---

### POST /api/channels/refresh-names

Refresh all channel names by querying the Discord API. Channels that no longer exist on Discord are automatically removed from the database.

**Response**

```json
{
  "success": true,
  "updated_count": 3,
  "removed_count": 1,
  "total_channels": 4,
  "errors": ["Failed to refresh channel 987654321098765432"]
}
```

**Notes**

- `errors` is only present when some channels failed to refresh.
- Channels deleted from Discord are removed from the local database.

| Status Code | Meaning |
|-------------|---------|
| 200 | Refresh complete |
| 500 | Internal error |

---

## Settings

### GET /api/settings

Get all application settings in a single response.

**Response**

```json
{
  "discord": {
    "application_id": "123456789012345678",
    "bot_token": "••••••••",
    "guild_id": "123456789012345678",
    "announcement_role_id": "123456789012345678",
    "mention_everyone": false,
    "event_duration_minutes": 60,
    "match_reminder_minutes": 15,
    "player_reminder_minutes": 5,
    "announcer_voice": "default",
    "voice_announcements_enabled": true
  },
  "announcer": {
    "announcer_voice": "default",
    "voice_announcements_enabled": true
  },
  "ui": {
    "auto_refresh_interval_seconds": 30
  },
  "scheduler": {
    "match_check_cron": "0 * * * * *",
    "cleanup_check_cron": "0 0 * * * *",
    "channel_refresh_cron": "0 0 * * * *"
  },
  "voices": [
    {
      "id": "default",
      "name": "Default",
      "path": "/voices/default.mp3"
    }
  ]
}
```

**Notes**

- `bot_token` is always masked in responses for security.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### GET /api/settings/discord

Get Discord bot settings only.

**Response**

Same as the `"discord"` object from `GET /api/settings`.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/settings/discord

Update Discord bot settings. Automatically restarts the Discord bot process after saving.

**Request Body**

```json
{
  "application_id": "123456789012345678",
  "bot_token": "MTIzNDU2Nzg5...",
  "guild_id": "123456789012345678",
  "announcement_role_id": "123456789012345678",
  "mention_everyone": false,
  "event_duration_minutes": 60,
  "match_reminder_minutes": 15,
  "player_reminder_minutes": 5,
  "announcer_voice": "default",
  "voice_announcements_enabled": true,
  "voice_channel_category_id": "123456789012345678",
  "voice_channel_cleanup_delay_minutes": 5
}
```

All fields are optional; omitted fields remain unchanged.

**Notes**

- Send the full unmasked token when updating `bot_token`. Do not send the masked `"••••••••"` value.
- After saving, the Discord bot process is restarted (via PM2 in dev, pkill in production Docker).

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated and bot restarted |
| 500 | Save failed |

---

### GET /api/settings/announcer

Get voice announcer settings.

**Response**

```json
{
  "announcer_voice": "default",
  "voice_announcements_enabled": true,
  "match_start_delay_seconds": 5
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/settings/announcer

Update voice announcer settings.

**Request Body**

```json
{
  "announcer_voice": "default",
  "voice_announcements_enabled": true,
  "match_start_delay_seconds": 5
}
```

| Field | Type | Validation |
|-------|------|------------|
| `announcer_voice` | string | Valid voice ID |
| `voice_announcements_enabled` | boolean | — |
| `match_start_delay_seconds` | number | 0–300 inclusive |

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 400 | `match_start_delay_seconds` out of range |

---

### GET /api/settings/ui

Get UI display settings.

**Response**

```json
{
  "auto_refresh_interval_seconds": 30
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/settings/ui

Update UI display settings.

**Request Body**

```json
{
  "auto_refresh_interval_seconds": 30
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `auto_refresh_interval_seconds` | number | Yes | 5–300 inclusive |

**Response**

```json
{ "message": "UI settings updated successfully" }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 400 | Value out of range |

---

### GET /api/settings/scheduler

Get cron job schedule settings.

**Response**

```json
{
  "match_check_cron": "0 * * * * *",
  "cleanup_check_cron": "0 0 * * * *",
  "channel_refresh_cron": "0 0 * * * *"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/settings/scheduler

Update cron job schedules. Cron expressions must use the 6-part format (`second minute hour day month weekday`).

**Request Body**

```json
{
  "match_check_cron": "0 * * * * *",
  "cleanup_check_cron": "0 0 * * * *",
  "channel_refresh_cron": "0 0 * * * *"
}
```

All fields are optional. Any provided expression must have exactly 6 space-separated parts.

**Response**

Returns the full updated scheduler settings object (same as GET response).

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 400 | Invalid cron expression (must have 6 parts) |

---

### GET /api/settings/log-level

Get the current application log level.

**Response**

```json
{
  "log_level": "warning"
}
```

Valid values: `"debug"`, `"info"`, `"warning"`, `"error"`, `"critical"`.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/settings/log-level

Update the application log level. Takes effect within 5 seconds.

**Request Body**

```json
{
  "log_level": "info"
}
```

| Field | Type | Required | Valid Values |
|-------|------|----------|-------------|
| `log_level` | string | Yes | `"debug"`, `"info"`, `"warning"`, `"error"`, `"critical"` |

**Response**

```json
{
  "success": true,
  "log_level": "info"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 400 | Invalid log level value |

---

## Welcome Flow

### GET /api/welcome-flow

Check whether the initial setup wizard has been completed.

**Response**

```json
{
  "isFirstRun": false,
  "completed": true,
  "dbReady": true,
  "metadata": {
    "screens_completed": [1, 2, 3],
    "completion_date": "2024-01-01T00:00:00.000Z",
    "setup_type": "standard"
  }
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### PUT /api/welcome-flow

Mark the welcome wizard as completed.

**Request Body**

```json
{
  "setupType": "standard"
}
```

| Field | Type | Required | Valid Values |
|-------|------|----------|-------------|
| `setupType` | string | Yes | `"pro_mode"`, `"standard"` |

**Response**

```json
{ "success": true }
```

**Notes**

- Sets a `welcome_flow_completed` cookie valid for 30 days.

| Status Code | Meaning |
|-------------|---------|
| 200 | Completed |
| 400 | Missing or invalid `setupType` |

---

### PUT /api/welcome-flow/screen

Record which screen the user has reached in the welcome wizard.

**Request Body**

```json
{
  "screen": 2
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `screen` | number | Yes | Current screen number |

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Saved |

---

## Upload

### POST /api/upload/event-image

Upload an image for a match or tournament event. Accepts `multipart/form-data`.

**Request**

Form field `image` containing the image file.

| Constraint | Value |
|------------|-------|
| Allowed types | JPEG, PNG, WebP, GIF |
| Max file size | 5 MB |

File signatures are verified server-side (not just MIME type).

**Response**

```json
{
  "success": true,
  "imageUrl": "/uploads/events/abc123.jpg",
  "filename": "abc123.jpg"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Uploaded |
| 400 | Missing file, invalid type, or file too large |

---

### DELETE /api/upload/event-image

Delete a previously uploaded event image.

**Query Parameters**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `imageUrl` | string | Yes | Full relative URL (e.g., `/uploads/events/abc123.jpg`) |

**Notes**

- The `imageUrl` must start with `/uploads/events/`. Requests targeting other paths are rejected.

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Deleted |
| 400 | Missing or invalid `imageUrl` |

---

## Matches

Match status follows this progression:

```
created → gather → assign → battle → complete
                                   ↘ cancelled
```

### GET /api/matches

List matches. Returns active matches by default.

**Query Parameters**

| Parameter | Value | Description |
|-----------|-------|-------------|
| `status` | `"complete"` | Return completed matches instead of active ones |

**Response**

```json
[
  {
    "id": "1",
    "name": "Grand Finals",
    "description": "Best of 5",
    "status": "battle",
    "game_id": "overwatch-2",
    "game_name": "Overwatch 2",
    "game_icon": "/games/overwatch-2/icon.png",
    "game_color": "#F99E1A",
    "map_codes_supported": true,
    "maps": ["king-s-row", "hanamura"],
    "map_codes": { "king-s-row": "ABCD1234" },
    "map_id": "competitive",
    "map_name": "Competitive",
    "start_time": "2024-12-31T20:00:00.000Z",
    "start_date": "2024-12-31T20:00:00.000Z",
    "rules": "competitive",
    "rounds": 5,
    "livestream_link": "https://twitch.tv/example",
    "blue_team_voice_channel": "123456789012345678",
    "red_team_voice_channel": "123456789012345679",
    "tournament_name": "Winter Cup",
    "tournament_round": 3,
    "tournament_bracket_type": "winners",
    "participant_count": 10,
    "created_at": "2024-12-01T00:00:00.000Z",
    "updated_at": "2024-12-31T20:00:00.000Z"
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### POST /api/matches

Create a new match.

**Request Body**

```json
{
  "name": "Grand Finals",
  "description": "Best of 5",
  "gameId": "overwatch-2",
  "gameModeId": "competitive",
  "mapId": "king-s-row",
  "maps": ["king-s-row", "hanamura", "blizzard-world"],
  "rules": "competitive",
  "rounds": 5,
  "startDate": "2024-12-31T20:00:00.000Z",
  "livestreamLink": "https://twitch.tv/example",
  "announcements": [
    { "minutesBefore": 60, "message": "Match starts in 1 hour!" },
    { "minutesBefore": 5,  "message": "Match starting soon!" }
  ],
  "playerNotifications": true,
  "eventImageUrl": "/uploads/events/abc123.jpg"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Match display name |
| `gameId` | string | Yes | Game identifier |
| `gameModeId` | string | Yes | Game mode identifier |
| `description` | string | No | Match description |
| `mapId` | string | No | Single map identifier |
| `maps` | string[] | No | Ordered list of map IDs for multi-map matches |
| `rules` | `"casual"` \| `"competitive"` | No | Ruleset |
| `rounds` | number | No | Number of rounds/maps |
| `startDate` | ISO8601 | No | Scheduled start time |
| `livestreamLink` | string | No | Livestream URL |
| `announcements` | object[] | No | Scheduled announcement messages |
| `playerNotifications` | boolean | No | Whether to send reminders to participants |
| `eventImageUrl` | string | No | URL from upload endpoint |

**Response**

Created match object (201 status, same shape as list item).

| Status Code | Meaning |
|-------------|---------|
| 201 | Created |
| 400 | Validation error |

---

### GET /api/matches/{matchId}

Get a single match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

Single match object. Includes `tournament_allow_match_editing` boolean when the match is part of a tournament.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### PUT /api/matches/{matchId}

Update a match. Only allowed before the match reaches `"battle"` status. Tournament matches may additionally be restricted by `allow_match_editing`.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Request Body**

```json
{
  "name": "Updated Name",
  "description": "Updated description",
  "startDate": "2025-01-01T20:00:00.000Z",
  "rules": "casual",
  "rounds": 3,
  "livestreamLink": "https://twitch.tv/updated",
  "maps": ["hanamura", "ilios"]
}
```

All fields are optional.

**Response**

Updated match object.

| Status Code | Meaning |
|-------------|---------|
| 200 | Updated |
| 403 | Match is in `battle`/`complete`/`cancelled` status, or tournament disallows editing |
| 404 | Match not found |

---

### DELETE /api/matches/{matchId}

Delete a match and clean up associated resources.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Notes**

- Deletes the event image if one was uploaded.
- Queues Discord message deletions for any announcements that were sent.

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Deleted |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/participants

List all participants signed up for a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Notes**

- Participants are added and removed via Discord bot commands, not via REST API.

**Response**

```json
{
  "participants": [
    {
      "id": "1",
      "user_id": "user-abc",
      "discord_user_id": "123456789012345678",
      "username": "Player1",
      "avatar_url": "https://cdn.discordapp.com/avatars/...",
      "joined_at": "2024-12-31T18:00:00.000Z",
      "signup_data": {},
      "team_assignment": "blue",
      "receives_map_codes": true
    }
  ],
  "signupConfig": {}
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/games

Get the individual games (rounds) within a match. Auto-initializes game entries if none exist.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

```json
{
  "success": true,
  "games": [
    {
      "id": "1",
      "match_id": "1",
      "round": 1,
      "participant1_id": "1",
      "participant2_id": "2",
      "map_id": "king-s-row",
      "winner_id": null,
      "status": "pending",
      "created_at": "2024-12-31T20:00:00.000Z"
    }
  ]
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/games-with-results

Get match games along with full result details (scores, winner, etc.).

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

Array of game objects with result data merged in.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### POST /api/matches/{matchId}/assign-teams

Assign participants to Blue, Red, or Reserve teams.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Request Body**

```json
{
  "teamAssignments": [
    {
      "participantId": "1",
      "team": "blue",
      "receives_map_codes": true
    },
    {
      "participantId": "2",
      "team": "red",
      "receives_map_codes": false
    }
  ],
  "blueTeamVoiceChannel": "123456789012345678",
  "redTeamVoiceChannel": "123456789012345679"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `teamAssignments` | object[] | Yes | List of participant → team assignments |
| `teamAssignments[].participantId` | string | Yes | Participant ID |
| `teamAssignments[].team` | `"blue"` \| `"red"` \| `"reserve"` | Yes | Team assignment |
| `teamAssignments[].receives_map_codes` | boolean | No | Whether this player receives map codes |
| `blueTeamVoiceChannel` | string | No | Discord voice channel ID for Blue team |
| `redTeamVoiceChannel` | string | No | Discord voice channel ID for Red team |

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Assigned |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/overall-score

Get the aggregate score for a match (Blue vs Red across all games).

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

Scoring object with aggregate results for both teams.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### POST /api/matches/{matchId}/transition

Transition a match to a new status.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Request Body**

```json
{
  "newStatus": "gather"
}
```

| Field | Type | Required | Valid Values |
|-------|------|----------|-------------|
| `newStatus` | string | Yes | `"created"`, `"gather"`, `"assign"`, `"battle"`, `"complete"`, `"cancelled"` |

**Response**

Updated match object.

| Status Code | Meaning |
|-------------|---------|
| 200 | Transitioned |
| 400 | Invalid transition or status |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/reminders

List all scheduled announcements and reminders for a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

```json
{
  "match": {
    "id": "1",
    "name": "Grand Finals",
    "start_date": "2024-12-31T20:00:00.000Z",
    "player_notifications": true,
    "status": "gather"
  },
  "reminders": [
    {
      "reminder_time": "2024-12-31T19:00:00.000Z",
      "message": "Match starts in 1 hour!",
      "status": "pending"
    }
  ],
  "reminderCount": 1
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### POST /api/matches/{matchId}/map-codes

Save map codes for the match's maps. Codes are shared with players who have `receives_map_codes = true`.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Request Body**

```json
{
  "mapCodes": {
    "king-s-row": "ABCD1234",
    "hanamura": "EFGH5678"
  }
}
```

Map code values are capped at 24 characters each.

**Response**

```json
{
  "success": true,
  "mapCodes": {
    "king-s-row": "ABCD1234",
    "hanamura": "EFGH5678"
  }
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Saved |
| 400 | Code exceeds 24 characters |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/map-codes

Retrieve saved map codes for a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

```json
{
  "mapCodes": {
    "king-s-row": "ABCD1234"
  }
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### POST /api/matches/{matchId}/map-notes

Save a note for a specific map within the match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Request Body**

```json
{
  "mapId": "king-s-row",
  "note": "Blue team focuses on point A"
}
```

| Field | Type | Required |
|-------|------|----------|
| `mapId` | string | Yes |
| `note` | string | No |

**Response**

```json
{
  "success": true,
  "note": "Blue team focuses on point A"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Saved |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/map-notes

Retrieve all map notes for a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |

**Response**

```json
{
  "notes": {
    "king-s-row": "Blue team focuses on point A"
  }
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match not found |

---

### GET /api/matches/{matchId}/games/{gameId}/result

Get the recorded result of a single game within a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |
| `gameId` | string | Game ID |

**Response**

Result object with scores and winner.

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Match or game not found, or no result recorded yet |

---

### POST /api/matches/{matchId}/games/{gameId}/result

Record the result (winner) for a single game within a match.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `matchId` | string | Match ID |
| `gameId` | string | Game ID |

**Request Body**

```json
{
  "gameId": "1",
  "matchId": "1",
  "winner": "team1"
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gameId` | string | Yes | Must match URL parameter |
| `matchId` | string | Yes | Must match URL parameter |
| `winner` | `"team1"` \| `"team2"` | Yes | Winning team |

**Response**

```json
{
  "success": true,
  "message": "Blue Team wins!"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Result recorded |
| 400 | Validation error or mismatched IDs |
| 404 | Match or game not found |

---

## Tournaments

Tournament status follows the same progression as matches:

```
created → gather → assign → battle → complete
                                   ↘ cancelled
```

### GET /api/tournaments

List tournaments. Returns active tournaments by default.

**Query Parameters**

| Parameter | Value | Description |
|-----------|-------|-------------|
| `status` | `"complete"` | Return completed tournaments instead of active ones |

**Response**

```json
[
  {
    "id": "1",
    "name": "Winter Cup",
    "description": "Annual championship",
    "status": "battle",
    "game_id": "overwatch-2",
    "game_name": "Overwatch 2",
    "game_icon": "/games/overwatch-2/icon.png",
    "game_color": "#F99E1A",
    "game_mode_id": "competitive",
    "format": "single-elimination",
    "rounds_per_match": 3,
    "ruleset": "competitive",
    "max_participants": 64,
    "allow_player_team_selection": false,
    "allow_match_editing": true,
    "event_image_url": "/uploads/events/abc123.jpg",
    "start_date": "2025-01-01T00:00:00.000Z",
    "start_time": "2025-01-01T18:00:00.000Z",
    "participant_count": 48,
    "has_bracket": true,
    "created_at": "2024-12-01T00:00:00.000Z",
    "updated_at": "2024-12-31T00:00:00.000Z"
  }
]
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |

---

### POST /api/tournaments

Create a new tournament.

**Request Body**

```json
{
  "name": "Winter Cup",
  "description": "Annual championship",
  "gameId": "overwatch-2",
  "gameModeId": "competitive",
  "format": "single-elimination",
  "startDate": "2025-01-01T00:00:00.000Z",
  "startTime": "2025-01-01T18:00:00.000Z",
  "roundsPerMatch": 3,
  "ruleset": "competitive",
  "maxParticipants": 64,
  "eventImageUrl": "/uploads/events/abc123.jpg",
  "allowPlayerTeamSelection": false,
  "allowMatchEditing": true
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Tournament display name |
| `gameId` | string | Yes | Game identifier |
| `gameModeId` | string | Yes | Game mode identifier |
| `format` | `"single-elimination"` \| `"double-elimination"` | Yes | Bracket format |
| `roundsPerMatch` | number | Yes | Games per bracket match |
| `description` | string | No | Tournament description |
| `startDate` | ISO8601 | No | Start date |
| `startTime` | ISO8601 | No | Start time |
| `ruleset` | string | No | Rule set label |
| `maxParticipants` | number | No | Participant cap |
| `eventImageUrl` | string | No | URL from upload endpoint |
| `allowPlayerTeamSelection` | boolean | No | Allow players to choose their team |
| `allowMatchEditing` | boolean | No | Allow editing matches after creation |

**Response**

Created tournament object (201 status, same shape as list item).

| Status Code | Meaning |
|-------------|---------|
| 201 | Created |
| 400 | Validation error |

---

### GET /api/tournaments/{tournamentId}

Get full tournament details including teams and participants.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Response**

Tournament object with an additional `teams` array:

```json
{
  "teams": [
    {
      "id": "1",
      "tournament_id": "1",
      "team_name": "Team Alpha",
      "created_at": "2024-12-01T00:00:00.000Z",
      "members": [
        {
          "id": "1",
          "team_id": "1",
          "user_id": "user-abc",
          "discord_user_id": "123456789012345678",
          "username": "Player1",
          "avatar_url": "https://cdn.discordapp.com/avatars/...",
          "joined_at": "2024-12-15T00:00:00.000Z"
        }
      ]
    }
  ]
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Tournament not found |

---

### DELETE /api/tournaments/{tournamentId}

Delete a tournament and all associated data.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Notes**

- Queues Discord message deletions for tournament and all associated matches.
- Deletes bracket matches, teams, and participants.

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Deleted |
| 404 | Tournament not found |

---

### GET /api/tournaments/{tournamentId}/participants

Get all participants signed up for a tournament.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Response**

```json
{
  "participants": [
    {
      "id": "1",
      "user_id": "user-abc",
      "username": "Player1",
      "joined_at": "2024-12-15T00:00:00.000Z",
      "team_assignment": "Team Alpha",
      "signup_data": {}
    }
  ],
  "signupConfig": {}
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Tournament not found |

---

### GET /api/tournaments/{tournamentId}/teams

Get all teams in a tournament with their members.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Response**

Array of team objects (same shape as the `teams` array in the tournament detail response).

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Tournament not found |

---

### POST /api/tournaments/{tournamentId}/teams

Create a new team in the tournament.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Request Body**

```json
{
  "teamName": "Team Alpha"
}
```

| Field | Type | Required |
|-------|------|----------|
| `teamName` | string | Yes |

**Response**

Created team object with an empty `members` array (201 status).

| Status Code | Meaning |
|-------------|---------|
| 201 | Created |
| 409 | Team name already exists in this tournament |

---

### PUT /api/tournaments/{tournamentId}/teams

Assign participants to teams. Clears all previous team assignments before applying the new ones.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Request Body**

```json
{
  "teams": [
    {
      "teamId": "1",
      "members": [
        { "userId": "user-abc", "username": "Player1" },
        { "userId": "user-def", "username": "Player2" }
      ]
    }
  ]
}
```

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Assigned |
| 404 | Tournament or team not found |

---

### DELETE /api/tournaments/{tournamentId}/teams

Delete a specific team from the tournament.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Query Parameters**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `teamId` | string | Yes | Team ID to delete |

**Response**

```json
{ "success": true }
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Deleted |
| 400 | Missing `teamId` |
| 404 | Team not found |

---

### GET /api/tournaments/{tournamentId}/matches

Get all bracket matches for a tournament.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Response**

```json
{
  "matches": [
    {
      "id": "1",
      "round": 1,
      "bracket_type": "winners",
      "team1": { "id": "1", "name": "Team Alpha" },
      "team2": { "id": "2", "name": "Team Beta" },
      "winner": null,
      "status": "ongoing",
      "rawStatus": "battle",
      "match_order": 1
    }
  ],
  "count": 4
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Tournament not found |

---

### GET /api/tournaments/{tournamentId}/standings

Get win/loss standings for all teams in the tournament.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Response**

```json
{
  "standings": [
    {
      "team_id": "1",
      "team_name": "Team Alpha",
      "matches_played": 3,
      "wins": 3,
      "losses": 0
    }
  ]
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Success |
| 404 | Tournament not found |

---

### POST /api/tournaments/{tournamentId}/transition

Transition a tournament to a new status.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Request Body**

```json
{
  "newStatus": "battle"
}
```

| Field | Type | Required | Valid Values |
|-------|------|----------|-------------|
| `newStatus` | string | Yes | `"created"`, `"gather"`, `"assign"`, `"battle"`, `"complete"`, `"cancelled"` |

**Notes**

- Transitioning to `"battle"` requires at least 2 teams with members assigned.

**Response**

Updated tournament object.

| Status Code | Meaning |
|-------------|---------|
| 200 | Transitioned |
| 400 | Invalid transition, or requirements not met (e.g., insufficient teams) |
| 404 | Tournament not found |

---

### POST /api/tournaments/{tournamentId}/bracket-assignments

Save the seeding positions for each team before generating bracket matches.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Request Body**

```json
{
  "assignments": [
    { "position": 1, "teamId": "1" },
    { "position": 2, "teamId": "2" }
  ]
}
```

**Notes**

- Tournament must be in `"assign"` status.

**Response**

```json
{
  "message": "Bracket assignments saved successfully",
  "assignments": [...]
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Saved |
| 400 | Tournament not in `assign` status |
| 404 | Tournament not found |

---

### POST /api/tournaments/{tournamentId}/generate-matches

Generate bracket matches for the tournament. If bracket assignments are provided, they are applied first; otherwise teams are seeded automatically.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Request Body** (optional)

```json
{
  "assignments": [
    { "position": 1, "teamId": "1" },
    { "position": 2, "teamId": "2" }
  ]
}
```

**Notes**

- Tournament must be in `"assign"` status.
- Returns an error if bracket matches have already been generated.

**Response**

```json
{
  "message": "Bracket generated successfully",
  "matchCount": 8,
  "format": "single-elimination",
  "tournamentId": "1"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Bracket generated |
| 400 | Not in `assign` status or matches already generated |
| 404 | Tournament not found |

---

### POST /api/tournaments/{tournamentId}/progress

Advance the tournament bracket to the next round, or finalize the tournament if all rounds are complete.

**URL Parameters**

| Parameter | Type | Description |
|-----------|------|-------------|
| `tournamentId` | string | Tournament ID |

**Notes**

- Tournament must be in `"battle"` status.
- All matches in the current round must be completed before progressing.
- Handles both single-elimination and double-elimination bracket logic.

**Response** (new round generated)

```json
{
  "message": "Next round generated",
  "matchCount": 4,
  "nextRound": 2,
  "tournamentId": "1"
}
```

**Response** (tournament complete)

```json
{
  "message": "Tournament complete",
  "tournamentId": "1",
  "winner": "Team Alpha"
}
```

| Status Code | Meaning |
|-------------|---------|
| 200 | Progressed or tournament finalized |
| 400 | Not in `battle` status or current round not fully complete |
| 404 | Tournament not found |
