Overview
Base URL
us-central1-mini4wdtournament.cloudfunctions.net
Protocol
HTTPS only
Format
application/json
Authentication
All requests require a permanent API key passed as a Bearer token.
Authorization: Bearer m4wd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Generating a Key
- Log in to the app and click your profile icon in the top-right corner
- Select Generate API Key
- Copy the key immediately — it is shown only once and never stored in plain text
- Store it securely in an environment variable, never in source control
Plan Limits
| Limit | Free | Pro |
|---|---|---|
| Max Tournaments | 10 | Unlimited |
| API Key | 1 active key | 1 active key |
| Real-time dashboard updates | ✅ | ✅ |
Create Tournament
/apiCreateTournament
Required Headers
| Header | Value |
|---|---|
Authorization | Bearer <your_api_key> |
Content-Type | application/json |
Parameters
You must pass the tournament configuration as a JSON body (POST).
{
"name": "My Tournament",
"mode": "STANDARD",
"dryRun": false,
"settings": { ... },
"entries": [ ... ]
}
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string |
Yes | Display name of the tournament |
mode | string |
Yes | STANDARD, TICKETED, or POINTS |
externalId | string |
No | Your app's own ID for this tournament. Stored for cross-reference; not validated for uniqueness. |
settings | object |
No | Tournament configuration. Available fields depend on mode. See settings fields sections below. |
entries | array |
No | List of player registrations to pre-populate. See Entries Array. |
Entries Array
Each object in entries registers a player and their cars or tickets.
| Field | Type | Required | Description |
|---|---|---|---|
playerName | string |
Yes | Player display name. Same player can enter multiple categories — matched case-insensitively. |
category | string |
Yes | Category key. Must be in enabledCategories (unless dynamic categories enabled). |
count | number |
Yes | STANDARD: Number of cars. TICKETED: Number of tickets. Must be ≥ 1 and ≤ configured max. POINTS: Not needed, pass 0. |
externalId | string |
No | Your app's own ID for this player. Stored for cross-reference. |
Known Category Keys
box-stock
Box Stock
stock
Stock
bmax
BMax
open
Open Class
"dynamicCategoriesEnabled": true to allow custom category names in entries. Unknown names are automatically slugified and created in the tournament.
Standard Mode
Round-robin style tournament where players register cars. Cars race across rounds and accumulate wins and losses. Supports second-chance (double elimination) brackets.
Settings Fields
| Field | Type | Default | Description |
|---|---|---|---|
lanes | number | 3 | Number of racing lanes (3 or 5) |
enabledCategories | string[] | ["box-stock","stock","bmax","open"] | Active racing categories |
maxCarsPerCategory | object | {"box-stock":3,...} | Max cars a single player can enter per category. Key = category slug, value = max count. |
secondChanceMode | string | "everyone" | "everyone", "none", or "cutoff" |
secondChanceThreshold | number | 3 | Losses before elimination (when secondChanceMode is "cutoff") |
timerEnabled | boolean | false | Show lap timer during races |
confirmWinnerSelection | boolean | true | Confirmation dialog before saving race results |
dynamicCategoriesEnabled | boolean | false | Auto-create unknown categories from entries |
reEntryEnabled | boolean | false | Allow eliminated players to re-enter. Only valid when secondChanceMode is "none". |
reEntryMaxCars | number | 2 | Max re-entries per player. Only applies when reEntryEnabled is true. |
rebuy_enabled, rebuy_ticket_amount, boss_battle_enabled, pointsHeats, pointsDistribution, topCutSize, maxTicketsPerCategory
Sample Payload
{
"name": "Summer Cup 2025",
"mode": "STANDARD",
"dryRun": false,
"externalId": "my-app-event-001",
"settings": {
"lanes": 3,
"enabledCategories": ["box-stock", "bmax"],
"maxCarsPerCategory": { "box-stock": 3, "bmax": 2 },
"secondChanceMode": "everyone",
"secondChanceThreshold": 3,
"timerEnabled": true,
"confirmWinnerSelection": true
},
"entries": [
{ "playerName": "Racer X", "category": "box-stock", "count": 2 },
{ "playerName": "Speedy", "category": "bmax", "count": 1 },
{ "playerName": "Turbo King", "category": "box-stock", "count": 3 }
]
}
Ticketed Mode
Ticket-based bracket tournament. Players enter with tickets instead of cars. Eliminated when tickets run out. Supports rebuys and boss battles.
count in each entry represents number of tickets, not cars. One car entity is created per player assigned that many tickets.
Settings Fields
| Field | Type | Default | Description |
|---|---|---|---|
lanes | number | 3 | Number of racing lanes (3 or 5) |
enabledCategories | string[] | all 4 categories | Active categories |
maxTicketsPerCategory | object | {"box-stock":3,...} | Max tickets a player can buy per category |
rebuy_enabled | boolean | false | Allow players to purchase additional tickets mid-event |
rebuy_ticket_amount | number | 1 | Tickets granted on rebuy |
boss_battle_enabled | boolean | true | Include a Boss Battle round |
timerEnabled | boolean | false | Show lap timer |
confirmWinnerSelection | boolean | true | Confirm dialog before saving results |
dynamicCategoriesEnabled | boolean | false | Auto-create unknown categories |
secondChanceMode, secondChanceThreshold, reEntryEnabled, reEntryMaxCars, pointsHeats, pointsDistribution, topCutSize, maxCarsPerCategory
Sample Payload
{
"name": "Grand Prix Qualifier",
"mode": "TICKETED",
"dryRun": false,
"externalId": "my-app-event-002",
"settings": {
"lanes": 3,
"enabledCategories": ["stock", "open"],
"maxTicketsPerCategory": { "stock": 5, "open": 3 },
"rebuy_enabled": true,
"rebuy_ticket_amount": 1,
"boss_battle_enabled": true,
"reEntryEnabled": false
},
"entries": [
{ "playerName": "Ace", "category": "stock", "count": 3 },
{ "playerName": "Blitz", "category": "open", "count": 2 },
{ "playerName": "Circuit", "category": "stock", "count": 5 }
]
}
Points Mode
Heat-based accumulation tournament. Players earn points across multiple heats based on finishing position. Supports an optional top-cut playoff.
Settings Fields
| Field | Type | Default | Description |
|---|---|---|---|
lanes | number | 3 | Number of racing lanes (3 or 5) |
enabledCategories | string[] | all 4 categories | Active categories |
maxCarsPerCategory | object | {"box-stock":3,...} | Max cars per player per category |
pointsHeats | number | 3 | Number of heat rounds each car races |
pointsDistribution | string | "3,2,1" | Comma-separated points per finishing position (1st, 2nd, 3rd…) |
topCutSize | number | 0 | Top N cars advance to a playoff. 0 = AUTO |
timerEnabled | boolean | false | Show lap timer |
confirmWinnerSelection | boolean | true | Confirm dialog before saving results |
dynamicCategoriesEnabled | boolean | false | Auto-create unknown categories |
secondChanceMode, secondChanceThreshold, rebuy_enabled, rebuy_ticket_amount, boss_battle_enabled, reEntryEnabled, reEntryMaxCars, maxTicketsPerCategory
Sample Payload
{
"name": "Monthly Points Championship",
"mode": "POINTS",
"dryRun": false,
"externalId": "my-app-event-003",
"settings": {
"lanes": 5,
"enabledCategories": ["bmax", "open"],
"maxCarsPerCategory": { "bmax": 2, "open": 2 },
"pointsHeats": 3,
"pointsDistribution": "3,2,1",
"topCutSize": 4
},
"entries": [
{ "playerName": "Nova", "category": "bmax", "count": 0 },
{ "playerName": "Storm", "category": "open", "count": 0 },
{ "playerName": "Blaze", "category": "open", "count": 0 }
]
}
Get Tournament Results
Export match-by-match results programmatically from a tournament you own.
/apiGetTournamentResults
Required Headers
| Header | Value |
|---|---|
Authorization | Bearer <your_api_key> |
Content-Type | application/json (If using POST body) |
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tournamentId | string |
Yes | The ID of the tournament. Can be passed in the JSON body (POST) or as a query parameter (GET). |
format | string |
No | Set to csv to receive a text/csv string. Can be passed as a query parameter (GET). Defaults to JSON. |
{
"tournamentId": "YOUR_TOURNAMENT_ID_HERE"
}
GET /apiGetTournamentResults?tournamentId=YOUR_TOURNAMENT_ID_HERE&format=csv
200 OK Example Response (JSON)
{
"success": true,
"results": [
{
"category": "Box Stock",
"roundName": "Round 1",
"matchNum": "M1",
"laneNum": 1,
"playerName": "Racer X",
"externalId": "usr_123",
"result": "1st Place",
"time": "15.340s",
"carName": "Speed Demon"
}
]
}
200 OK Example Response (CSV)
category,roundName,matchNum,laneNum,playerName,externalId,result,time,carName
"Box Stock","Round 1","M1","1","Racer X","usr_123","1st Place","15.340s","Speed Demon"
"Box Stock","Round 1","M1","2","Dash","usr_456","2nd Place","16.100s","Shooting Star"
"Box Stock","Round 1","M1","3","Zoom","usr_789","3rd Place","16.850s","Thunderbolt"
carName field is only returned for Standard and Points modes. Ticketed mode does not track distinct car names per player.
Dry Run (Testing)
You can test your payload validity and simulate the response without actually creating a tournament by adding "dryRun": true to the root of your request JSON.
200 OK Dry Run Response
{
"success": true,
"dryRun": true,
"message": "Dry run successful. No tournament was created.",
"importStats": {
"successful": 4,
"failed": 0,
"errors": []
}
}
Responses
200 OK Success
{
"success": true,
"tournamentId": "abc123XYZ",
"importStats": {
"successful": 4,
"failed": 1,
"errors": [
"Entry 2: Player \"Storm\" exceeds max limit of 2 for category \"bmax\"."
]
}
}
Error Codes
| Status | Reason |
|---|---|
400 | Missing name/mode, invalid mode, or mode-specific forbidden field included |
401 | Missing, invalid, or revoked API key |
403 | Tournament limit reached (Free plan max: 10) |
404 | Tournament not found |
500 | Unexpected server error |
Code Examples
Node.js
// Store key in env: M4WD_API_KEY=m4wd_...
const API_KEY = process.env.M4WD_API_KEY;
const API_URL = 'https://us-central1-mini4wdtournament.cloudfunctions.net/apiCreateTournament';
async function createTournament(payload) {
const res = await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await res.json();
if (!data.success) throw new Error(data.error);
return data;
}
createTournament({
name: 'Club Night — Round 7',
mode: 'STANDARD',
dryRun: false,
settings: { lanes: 3, enabledCategories: ['box-stock'] },
entries: [{ playerName: 'Marc', category: 'box-stock', count: 2 }]
}).then(r => console.log('Created:', r.tournamentId));
Python
import os, requests
API_URL = 'https://us-central1-mini4wdtournament.cloudfunctions.net/apiCreateTournament'
API_KEY = os.environ['M4WD_API_KEY']
def create_tournament(payload):
res = requests.post(API_URL, json=payload, headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
})
data = res.json()
if not data.get('success'): raise Exception(data.get('error'))
return data
result = create_tournament({
'name': 'Python Test',
'mode': 'POINTS',
'dryRun': false,
'settings': { 'lanes': 5, 'pointsHeats': 3 },
'entries': [{ 'playerName': 'Alpha', 'category': 'bmax', 'count': 1 }]
})
print('Tournament ID:', result['tournamentId'])
cURL
curl -X POST \
https://us-central1-mini4wdtournament.cloudfunctions.net/apiCreateTournament \
-H "Authorization: Bearer m4wd_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "cURL Test Tournament",
"mode": "STANDARD",
"dryRun": false,
"settings": { "lanes": 3, "enabledCategories": ["box-stock"] },
"entries": [{ "playerName": "Test Racer", "category": "box-stock", "count": 1 }]
}'
Settings Quick Reference
| Field | Standard | Single Elim. | Points |
|---|---|---|---|
lanes | ✓ | ✓ | ✓ |
enabledCategories | ✓ | ✓ | ✓ |
timerEnabled | ✓ | ✓ | ✓ |
confirmWinnerSelection | ✓ | ✓ | ✓ |
dynamicCategoriesEnabled | ✓ | ✓ | ✓ |
maxCarsPerCategory | ✓ | ✗ | ✓ |
secondChanceMode | ✓ | ✗ | ✗ |
secondChanceThreshold | ✓ | ✗ | ✗ |
maxTicketsPerCategory | ✗ | ✓ | ✗ |
rebuy_enabled | ✗ | ✓ | ✗ |
rebuy_ticket_amount | ✗ | ✓ | ✗ |
boss_battle_enabled | ✗ | ✓ | ✗ |
reEntryEnabled | ✓ | ✗ | ✗ |
reEntryMaxCars | ✓ | ✗ | ✗ |
pointsHeats | ✗ | ✗ | ✓ |
pointsDistribution | ✗ | ✗ | ✓ |
topCutSize | ✗ | ✗ | ✓ |