ESFA Backend API Documentation Help

Finish Match

Submit final results for a completed game match, including player rankings and scores.

Endpoint Details

Method: POST
Path: /api/v1/games/{gameCode}/matches/{matchId}/finish
Authentication: Required (Bearer token)
Content-Type: application/json

Parameters

Path Parameters

Parameter

Type

Required

Description

gameCode

string

Yes

Code identifying the game type (e.g., "TTT" for Tic-Tac-Toe)

matchId

string

Yes

Unique identifier for the match (UUID format)

Headers

Header

Type

Required

Description

Authorization

string

Yes

Bearer token obtained from Authentication Service

Content-Type

string

Yes

Must be application/json

Request Body

Field

Type

Required

Description

results

array

Yes

Array of player results for the match

results[].playerId

string

Yes

Unique identifier for the player

results[].place

integer

Yes

Final ranking/position (1 = first place, 2 = second, etc.)

results[].score

number

Yes

Final score achieved by the player

Constraints

  • Place values: Must be positive integers starting from 1

  • Unique places: Each player must have a unique place (no ties)

  • Score values: Must be numeric (integers or decimals)

  • Player validation: All playerId values must correspond to players in the match

Request Examples

Basic Request Body

{ "results": [ { "playerId": "p1", "place": 1, "score": 1200 }, { "playerId": "p2", "place": 2, "score": 900 } ] }

cURL Example

curl -X POST \ -H "Authorization: Bearer eyJhbGciOi..." \ -H "Content-Type: application/json" \ -d '{ "results": [ {"playerId": "p1", "place": 1, "score": 1200}, {"playerId": "p2", "place": 2, "score": 900} ] }' \ https://api.your-service.tld/api/v1/games/TTT/matches/a3c6e5f2-1a2b-4c5d-8e9f-0123456789ab/finish

JavaScript (fetch)

const finishMatch = async (gameCode, matchId, results, token) => { const response = await fetch(`/api/v1/games/${gameCode}/matches/${matchId}/finish`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ results }) }); if (!response.ok) { throw new Error(`Failed to finish match: ${response.status}`); } return response; // 204 No Content };

Python Example

import requests def finish_match(game_code, match_id, results, token): url = f'https://api.your-service.tld/api/v1/games/{game_code}/matches/{match_id}/finish' headers = { 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } data = {'results': results} response = requests.post(url, json=data, headers=headers) response.raise_for_status() return response # Usage results = [ {'playerId': 'p1', 'place': 1, 'score': 1200}, {'playerId': 'p2', 'place': 2, 'score': 900} ] finish_match('TTT', 'a3c6e5f2-1a2b-4c5d-8e9f-0123456789ab', results, token)

Response

Success Response (204 No Content)

When the match results are successfully processed, the API returns:

HTTP/1.1 204 No Content

No response body - A 204 No Content status indicates successful processing.

Error Responses

400 Bad Request

Invalid request payload or business logic violation.

{ "errors": [ { "i18N": "INVALID_RESULTS", "error": "Invalid match results data", "type": "BadRequest" } ] }

Common causes:

  • Missing required fields (results, playerId, place, score)

  • Invalid data types (non-numeric scores, non-integer places)

  • Duplicate place values (two players with same ranking)

  • Invalid playerId (player not in match)

  • Negative or zero place values

  • Missing or empty results array

401 Unauthorized

Missing or invalid authentication token.

{ "errors": [ { "i18N": "UNAUTHORIZED", "error": "Invalid or missing authentication token", "type": "Unauthorized" } ] }

Solutions:

  • Verify the Authorization header is present and formatted correctly

  • Ensure the Bearer token is valid and not expired

  • Obtain a fresh token from the Authentication Service

404 Not Found

The specified match does not exist.

{ "errors": [ { "i18N": "MATCH_NOT_FOUND", "error": "Match not found", "type": "NotFound" } ] }

Common causes:

  • Invalid matchId (doesn't exist in the system)

  • Invalid gameCode (game type not supported)

  • Match may have been deleted or expired

409 Conflict

Match cannot be finished in its current state.

{ "errors": [ { "i18N": "MATCH_ALREADY_FINISHED", "error": "Match has already been finished", "type": "Conflict" } ] }

Common causes:

  • Match results already submitted

  • Match is not in a finishable state

  • Match has been cancelled or expired

500 Internal Server Error

Temporary server issue.

{ "errors": [ { "i18N": "SERVER_ERROR", "error": "Internal server error", "type": "ServerError" } ] }

Recommended action: Retry the request after a brief delay.

Usage Patterns

Basic Match Completion

async function completeMatch(gameCode, matchId, playerResults, token) { try { const response = await fetch(`/api/v1/games/${gameCode}/matches/${matchId}/finish`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ results: playerResults }) }); if (response.status === 204) { console.log('Match completed successfully'); return true; } else { const error = await response.json(); throw new Error(error.errors[0]?.error || 'Unknown error'); } } catch (error) { console.error('Failed to complete match:', error); return false; } }

Results Validation

function validateResults(results) { if (!Array.isArray(results) || results.length === 0) { throw new Error('Results must be a non-empty array'); } const places = new Set(); const playerIds = new Set(); for (const result of results) { // Validate required fields if (!result.playerId || typeof result.place !== 'number' || typeof result.score !== 'number') { throw new Error('Each result must have playerId, place, and score'); } // Validate place uniqueness and values if (places.has(result.place)) { throw new Error(`Duplicate place value: ${result.place}`); } if (result.place < 1 || !Number.isInteger(result.place)) { throw new Error(`Invalid place value: ${result.place}. Must be positive integer`); } places.add(result.place); // Validate player uniqueness if (playerIds.has(result.playerId)) { throw new Error(`Duplicate playerId: ${result.playerId}`); } playerIds.add(result.playerId); } // Validate place sequence (1, 2, 3, ..., n) const sortedPlaces = Array.from(places).sort((a, b) => a - b); for (let i = 0; i < sortedPlaces.length; i++) { if (sortedPlaces[i] !== i + 1) { throw new Error(`Missing place ${i + 1}. Places must be consecutive starting from 1`); } } return true; }

Tournament Results Processing

class TournamentManager { constructor(apiClient) { this.apiClient = apiClient; } async processMatchResults(match, gameResults) { try { // Convert game-specific results to API format const results = this.convertToApiResults(match.players, gameResults); // Validate before submission validateResults(results); // Submit to API await this.apiClient.finishMatch(match.gameCode, match.matchId, results); // Update local tournament state await this.updateTournamentStandings(match, results); console.log(`Match ${match.matchId} completed successfully`); return true; } catch (error) { console.error(`Failed to process match ${match.matchId}:`, error); return false; } } convertToApiResults(players, gameResults) { // Sort players by game performance const rankedPlayers = players .map(player => ({ ...player, gameScore: gameResults[player.id] || 0 })) .sort((a, b) => b.gameScore - a.gameScore); // Convert to API format with proper ranking return rankedPlayers.map((player, index) => ({ playerId: player.id, place: index + 1, score: player.gameScore })); } }

Retry Logic with Exponential Backoff

async function finishMatchWithRetry(gameCode, matchId, results, token, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await fetch(`/api/v1/games/${gameCode}/matches/${matchId}/finish`, { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ results }) }); if (response.status === 204) { return { success: true }; } // Don't retry client errors (4xx) if (response.status >= 400 && response.status < 500) { const errorData = await response.json(); return { success: false, error: errorData.errors[0]?.error || 'Client error', retryable: false }; } // Retry server errors (5xx) if (attempt === maxRetries) { return { success: false, error: 'Max retries exceeded', retryable: true }; } // Exponential backoff const delay = Math.pow(2, attempt) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); } catch (error) { if (attempt === maxRetries) { return { success: false, error: error.message, retryable: true }; } } } }

Integration Tips

Best Practices

  • Validate locally first: Check results format before API submission

  • Handle idempotency: Be prepared for duplicate submission scenarios

  • Log appropriately: Log match completion events for audit trails

  • Error handling: Distinguish between retryable and non-retryable errors

Performance Considerations

  • Batch processing: If finishing multiple matches, consider rate limiting

  • Connection reuse: Use HTTP connection pooling for better performance

  • Timeout handling: Set appropriate timeouts for match completion requests

Data Integrity

// Example: Ensure match data consistency async function safeFinishMatch(match, results, token) { // Pre-submission validation const matchPlayers = match.players.map(p => p.id); const resultPlayers = results.map(r => r.playerId); // Verify all players have results const missingPlayers = matchPlayers.filter(id => !resultPlayers.includes(id)); if (missingPlayers.length > 0) { throw new Error(`Missing results for players: ${missingPlayers.join(', ')}`); } // Verify no extra players const extraPlayers = resultPlayers.filter(id => !matchPlayers.includes(id)); if (extraPlayers.length > 0) { throw new Error(`Unknown players in results: ${extraPlayers.join(', ')}`); } // Submit results return await finishMatch(match.gameCode, match.matchId, results, token); }
05 September 2025