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) |
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
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