Documentation Index Fetch the complete documentation index at: https://docs.fkapi.sunr4y.dev/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The Football Kit Archive API uses standard HTTP status codes and returns consistent error responses in JSON format. All errors include a detail field with a human-readable error message.
All error responses follow this structure:
{
"detail" : "Error message description"
}
From fkapi/api.py:178-183:
## Error Responses
The API uses standard HTTP status codes:
- ` 200 OK ` : Successful request
- ` 400 Bad Request` : Invalid request parameters
- ` 401 Unauthorized` : Missing or invalid API key ( if authentication enabled)
- ` 403 Forbidden` : Rate limit exceeded
- ` 404 Not Found` : Resource not found
- ` 500 Internal Server Error` : Server error
Error responses follow this format :
```json
{
"detail" : "Error message description"
}
HTTP Status Codes
200 OK
Request succeeded. Returns the requested resource(s).
curl http://localhost:8000/api/kits/1
Response:
{
"id" : 1 ,
"name" : "Barcelona Home 2024-25" ,
"slug" : "barcelona-home-2024-25" ,
"team" : { ... },
"season" : { ... }
}
400 Bad Request
Invalid request parameters or validation errors.
Invalid Parameters
curl "http://localhost:8000/api/kits?page=invalid"
Response:
{
"detail" : "Invalid request parameters"
}
Invalid Color
From fkapi/api.py:834-836:
if primary_color not in AVAILABLE_COLORS :
raise ValidationError(
f "Invalid primary_color ' { primary_color } '. Available options: { ', ' .join( AVAILABLE_COLORS ) } "
)
curl "http://localhost:8000/api/kits?primary_color=InvalidColor"
Response:
{
"detail" : "Invalid primary_color 'InvalidColor'. Available options: White, Red, Blue, Black, Yellow, Green, Sky blue, Navy, Orange, Gray, Claret, Purple, Pink, Brown, Gold, Silver, Off-white"
}
Invalid Design
From fkapi/api.py:858-861:
design_normalized = design.strip()
if design_normalized not in AVAILABLE_DESIGNS :
raise ValidationError( f "Invalid design ' { design } '. Available options: { ', ' .join( AVAILABLE_DESIGNS ) } " )
curl "http://localhost:8000/api/kits?design=InvalidDesign"
Response:
{
"detail" : "Invalid design 'InvalidDesign'. Available options: Plain, Stripes, Graphic, Chest band, Contrasting sleeves, Pinstripes, Hoops, Single stripe, Half-and-half, Sash, Chevron, Checkers, Gradient, Diagonal, Cross, Quarters"
}
Invalid Season
From core/exceptions.py:57-70:
class InvalidSeasonError ( ScrapingError ):
"""Raised when a season format is invalid or cannot be parsed."""
curl "http://localhost:8000/api/seasons?id=invalid-season"
Response:
{
"detail" : "Invalid season format: invalid-season"
}
Bulk Kit Errors
From fkapi/api.py:1358-1361:
if len (slug_list) < 2 :
raise ValidationError( "Minimum 2 kits required" )
if len (slug_list) > 30 :
raise ValidationError( "Maximum 30 kits allowed" )
# Too few kits
curl "http://localhost:8000/api/kits/bulk?slugs=kit-1"
# Too many kits
curl "http://localhost:8000/api/kits/bulk?slugs=kit-1,kit-2,...,kit-31"
Response:
{
"detail" : "Minimum 2 kits required"
}
or
{
"detail" : "Maximum 30 kits allowed"
}
401 Unauthorized
Missing or invalid API key (only when authentication is enabled).
curl http://localhost:8000/api/kits/1
Response:
{
"detail" : "Missing or invalid API key"
}
Solution: Include a valid API key in the request header:
curl -H "X-API-Key: your-api-key-here" \
http://localhost:8000/api/kits/1
403 Forbidden
Rate limit exceeded.
From core/middleware.py:70-71:
if request_data[ "count" ] >= max_requests:
return HttpResponseForbidden( "Rate limit exceeded. Please try again later." )
# After exceeding 100 requests per hour
curl http://localhost:8000/api/kits/1
Response:
{
"detail" : "Rate limit exceeded. Please try again later."
}
Solution: Wait until the rate limit window resets (1 hour) or implement exponential backoff:
import time
import requests
def make_request_with_retry ( url , max_retries = 3 ):
for attempt in range (max_retries):
response = requests.get(url)
if response.status_code == 403 :
wait_time = 2 ** attempt # 1s, 2s, 4s
time.sleep(wait_time)
continue
return response
raise Exception ( "Max retries exceeded" )
404 Not Found
Requested resource does not exist.
Kit Not Found
From core/exceptions.py:25-38:
class KitNotFoundError ( ScrapingError ):
"""Raised when a kit is not found on the source."""
def __init__ ( self , slug : str , message : str | None = None ):
if message is None :
message = f "Kit not found: { slug } "
super (). __init__ (message, slug)
Error handler from fkapi/api.py:236-250:
@api.exception_handler (ScrapingError)
def scraping_error_handler ( request : HttpRequest, exc : ScrapingError) -> Any:
if isinstance (exc, KitNotFoundError):
return api.create_response(request, { "detail" : str (exc)}, status = 404 )
elif isinstance (exc, ClubNotFoundError):
return api.create_response(request, { "detail" : str (exc)}, status = 404 )
curl http://localhost:8000/api/kits/99999
Response:
{
"detail" : "Kit with ID 99999 not found"
}
Club Not Found
From core/exceptions.py:41-54:
class ClubNotFoundError ( ScrapingError ):
"""Raised when a club is not found on the source."""
def __init__ ( self , slug : str , message : str | None = None ):
if message is None :
message = f "Club not found: { slug } "
super (). __init__ (message, slug)
curl http://localhost:8000/api/clubs/99999/kits
Response:
{
"detail" : "Club with ID 99999 not found"
}
500 Internal Server Error
Unexpected server error.
From fkapi/api.py:253-264:
@api.exception_handler ( Exception )
def custom_exception_handler ( request : HttpRequest, exc : Exception ) -> Any:
logger = logging.getLogger( __name__ )
logger.error( f "Unhandled exception: { type (exc). __name__ } " , exc_info = True )
if settings. DEBUG :
detail = str (exc)
else :
detail = "An internal server error occurred. Please try again later."
return api.create_response(request, { "detail" : detail}, status = 500 )
Production Response:
{
"detail" : "An internal server error occurred. Please try again later."
}
Development Response (DEBUG=True):
{
"detail" : "Detailed error message with stack trace"
}
503 Service Unavailable
Database or cache connection failed.
From fkapi/api.py:301-308:
try :
Club.objects.count()
health_status[ "database" ] = "connected"
except Exception as e:
health_status[ "status" ] = "unhealthy"
health_status[ "database" ] = "disconnected"
health_status[ "error" ] = str (e)
return api.create_response(request, health_status, status = 503 )
curl http://localhost:8000/api/health
Response:
{
"status" : "unhealthy" ,
"timestamp" : "2026-03-03T12:00:00Z" ,
"database" : "disconnected" ,
"error" : "Connection refused"
}
Exception Classes
From core/exceptions.py:
ScrapingError (Base Exception)
class ScrapingError ( Exception ):
"""Base exception for scraping-related errors."""
def __init__ ( self , message : str , slug : str | None = None ):
self .message = message
self .slug = slug
super (). __init__ ( self .message)
KitNotFoundError
class KitNotFoundError ( ScrapingError ):
"""Raised when a kit is not found on the source."""
Status Code: 404
ClubNotFoundError
class ClubNotFoundError ( ScrapingError ):
"""Raised when a club is not found on the source."""
Status Code: 404
InvalidSeasonError
class InvalidSeasonError ( ScrapingError ):
"""Raised when a season format is invalid or cannot be parsed."""
Status Code: 400
RateLimitExceededError
class RateLimitExceededError ( ScrapingError ):
"""Raised when the rate limit for API requests is exceeded."""
def __init__ ( self , message : str | None = None ):
if message is None :
message = "Rate limit exceeded. Please try again later."
super (). __init__ (message, None )
Status Code: 403
ValidationError
class ValidationError ( ScrapingError ):
"""Raised when API request validation fails."""
def __init__ ( self , message : str | None = None ):
if message is None :
message = "Invalid request parameters"
super (). __init__ (message, None )
Status Code: 400
Error Handling Examples
Python
import requests
from typing import Optional
class APIError ( Exception ):
"""Base API error."""
pass
class NotFoundError ( APIError ):
"""Resource not found (404)."""
pass
class ValidationError ( APIError ):
"""Invalid request (400)."""
pass
class RateLimitError ( APIError ):
"""Rate limit exceeded (403)."""
pass
class ServerError ( APIError ):
"""Server error (500)."""
pass
class FootballKitAPI :
def __init__ ( self , base_url : str , api_key : Optional[ str ] = None ):
self .base_url = base_url
self .headers = {}
if api_key:
self .headers[ 'X-API-Key' ] = api_key
def _handle_response ( self , response : requests.Response):
"""Handle API response and raise appropriate errors."""
if response.status_code == 200 :
return response.json()
try :
error_data = response.json()
error_message = error_data.get( 'detail' , 'Unknown error' )
except :
error_message = response.text
if response.status_code == 400 :
raise ValidationError(error_message)
elif response.status_code == 401 :
raise APIError( f "Authentication failed: { error_message } " )
elif response.status_code == 403 :
raise RateLimitError(error_message)
elif response.status_code == 404 :
raise NotFoundError(error_message)
elif response.status_code >= 500 :
raise ServerError(error_message)
else :
raise APIError( f "HTTP { response.status_code } : { error_message } " )
def get_kit ( self , kit_id : int ):
"""Get kit by ID."""
response = requests.get(
f " { self .base_url } /kits/ { kit_id } " ,
headers = self .headers
)
return self ._handle_response(response)
def search_kits ( self , keyword : str ):
"""Search kits by keyword."""
response = requests.get(
f " { self .base_url } /kits/search" ,
params = { 'keyword' : keyword},
headers = self .headers
)
return self ._handle_response(response)
# Usage
api = FootballKitAPI( 'http://localhost:8000/api' )
try :
kit = api.get_kit( 1 )
print ( f "Found kit: { kit[ 'name' ] } " )
except NotFoundError as e:
print ( f "Kit not found: { e } " )
except ValidationError as e:
print ( f "Invalid request: { e } " )
except RateLimitError as e:
print ( f "Rate limited: { e } " )
# Wait and retry
except ServerError as e:
print ( f "Server error: { e } " )
# Log and alert
except APIError as e:
print ( f "API error: { e } " )
JavaScript
class APIError extends Error {
constructor ( message , statusCode ) {
super ( message );
this . statusCode = statusCode ;
this . name = 'APIError' ;
}
}
class NotFoundError extends APIError {
constructor ( message ) {
super ( message , 404 );
this . name = 'NotFoundError' ;
}
}
class ValidationError extends APIError {
constructor ( message ) {
super ( message , 400 );
this . name = 'ValidationError' ;
}
}
class RateLimitError extends APIError {
constructor ( message ) {
super ( message , 403 );
this . name = 'RateLimitError' ;
}
}
class ServerError extends APIError {
constructor ( message ) {
super ( message , 500 );
this . name = 'ServerError' ;
}
}
class FootballKitAPI {
constructor ( baseUrl , apiKey = null ) {
this . baseUrl = baseUrl ;
this . headers = {};
if ( apiKey ) {
this . headers [ 'X-API-Key' ] = apiKey ;
}
}
async _handleResponse ( response ) {
if ( response . ok ) {
return await response . json ();
}
let errorMessage ;
try {
const errorData = await response . json ();
errorMessage = errorData . detail || 'Unknown error' ;
} catch {
errorMessage = await response . text ();
}
switch ( response . status ) {
case 400 :
throw new ValidationError ( errorMessage );
case 401 :
throw new APIError ( `Authentication failed: ${ errorMessage } ` , 401 );
case 403 :
throw new RateLimitError ( errorMessage );
case 404 :
throw new NotFoundError ( errorMessage );
case 500 :
case 503 :
throw new ServerError ( errorMessage );
default :
throw new APIError ( `HTTP ${ response . status } : ${ errorMessage } ` , response . status );
}
}
async getKit ( kitId ) {
const response = await fetch ( ` ${ this . baseUrl } /kits/ ${ kitId } ` , {
headers: this . headers
});
return this . _handleResponse ( response );
}
async searchKits ( keyword ) {
const params = new URLSearchParams ({ keyword });
const response = await fetch ( ` ${ this . baseUrl } /kits/search? ${ params } ` , {
headers: this . headers
});
return this . _handleResponse ( response );
}
}
// Usage
const api = new FootballKitAPI ( 'http://localhost:8000/api' );
try {
const kit = await api . getKit ( 1 );
console . log ( `Found kit: ${ kit . name } ` );
} catch ( error ) {
if ( error instanceof NotFoundError ) {
console . log ( `Kit not found: ${ error . message } ` );
} else if ( error instanceof ValidationError ) {
console . log ( `Invalid request: ${ error . message } ` );
} else if ( error instanceof RateLimitError ) {
console . log ( `Rate limited: ${ error . message } ` );
// Wait and retry
} else if ( error instanceof ServerError ) {
console . log ( `Server error: ${ error . message } ` );
// Log and alert
} else {
console . log ( `API error: ${ error . message } ` );
}
}
Best Practices
1. Always Check Status Codes
response = requests.get(url)
if response.status_code != 200 :
# Handle error
print ( f "Error: { response.json().get( 'detail' ) } " )
2. Implement Retry Logic
import time
def make_request_with_retry ( url , max_retries = 3 ):
for attempt in range (max_retries):
try :
response = requests.get(url)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
if attempt == max_retries - 1 :
raise
time.sleep( 2 ** attempt) # Exponential backoff
3. Handle Rate Limits Gracefully
if response.status_code == 403 :
error = response.json()
if 'rate limit' in error.get( 'detail' , '' ).lower():
# Wait 60 seconds before retrying
time.sleep( 60 )
return make_request_with_retry(url)
4. Log Errors
import logging
logger = logging.getLogger( __name__ )
try :
kit = api.get_kit( 1 )
except APIError as e:
logger.error( f "API error: { e } " , exc_info = True )
# Handle error
5. Provide User-Friendly Messages
try :
kit = api.get_kit(kit_id)
except NotFoundError:
print ( f "Sorry, kit { kit_id } doesn't exist." )
except ValidationError as e:
print ( f "Invalid input: { e } " )
except RateLimitError:
print ( "Too many requests. Please try again in a few minutes." )
except ServerError:
print ( "Server is experiencing issues. Please try again later." )
Debugging Errors
Enable Debug Mode
In development, set DEBUG=True to get detailed error messages:
# settings.py
DEBUG = True
This will include stack traces in 500 error responses.
Check Server Logs
# Django development server
python manage.py runserver
# Production logs
tail -f /var/log/fkapi/error.log
Test Error Handling
# Test 404
curl http://localhost:8000/api/kits/99999
# Test 400
curl "http://localhost:8000/api/kits?primary_color=InvalidColor"
# Test 403 (after 100 requests)
for i in { 1..101} ; do
curl http://localhost:8000/api/health
done
Rate Limiting Learn about rate limits and 403 errors
Authentication Understand 401 authentication errors