Statbox Partner API

Complete public reference for the Stats API. This page is static HTML so ChatGPT and other readers can load every endpoint without JavaScript.

Styled UI: https://www.statbox.co/docs/api · Markdown: https://www.statbox.co/docs/api.md · llms.txt · Request a key: WhatsApp

What this API does

A partner sends a football, padel, or basketball game as a public Google Drive, YouTube, or Veo link, or asks Statbox to email an upload page. When the match is ready, Statbox emails a CSV of player stats plus highlight, screenshot, and game-highlight links. The partner then pulls that same game into their own app.

There is no venue camera step. Send location as the club or court name and pitch as the court name. Do not send a Statbox venue id. The ready email does not include a Statbox join link.

Base URL

https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api

Every call is this URL plus ?endpoint=NAME.

Authentication

Authorization: Bearer sbx_live_YOUR_API_KEY
Content-Type: application/json

Keys begin with sbx_live_. A key can be unlimited or capped to a number of games per calendar month (UTC). You can only read games created with that key.

Endpoints

MethodEndpointPurpose
POST?endpoint=stats-requestCreate a game from Drive/YouTube or ask for an upload-box email
GET or POST?endpoint=stats-statusneeds_upload, uploaded, or confirmed for that game
GET or POST?endpoint=stats-resultFull match JSON for that game only
GET or POST?endpoint=player-statsOne player from that game
GET or POST?endpoint=stats-csvSpreadsheet of that game
GET or POST?endpoint=games-leftHow many games this key can still create this month

1. POST ?endpoint=stats-request

Creates the Statbox game. Does not wait for stats.

Required

One of these two footage options (not both)

Optional

Example: YouTube

curl -X POST "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-request" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notify_email": "ops@partner.com",
    "location": "City Padel Club",
    "pitch": "Court 2",
    "footage_url": "https://youtu.be/VIDEO_ID"
  }'

Example: Veo

curl -X POST "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-request" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notify_email": "ops@partner.com",
    "location": "City Padel Club",
    "pitch": "Court 2",
    "footage_url": "https://app.veo.co/matches/MATCH_ID/"
  }'

Example: Google Drive

curl -X POST "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-request" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notify_email": "ops@partner.com",
    "location": "City Padel Club",
    "pitch": "Court 2",
    "footage_url": "https://drive.google.com/file/d/FILE_ID/view",
    "players": [
      { "name": "Ahmed Khalil", "team": "A", "external_id": "player-88" },
      { "name": "Omar Haddad", "team": "B", "external_id": "player-91" }
    ],
    "external_reference": "BOOKING-123"
  }'

Example: email an upload box

curl -X POST "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-request" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "notify_email": "ops@partner.com",
    "location": "City Padel Club",
    "pitch": "Court 2",
    "upload_via_email": true
  }'

Success response (HTTP 201)

{
  "match_id": "uuid",
  "key_code": "ABC123",
  "status": "uploaded",
  "notify_email": "ops@partner.com",
  "venue": { "name": "City Padel Club" },
  "pitch": { "name": "Court 2" },
  "footage_url": "https://youtu.be/VIDEO_ID",
  "footage_source": "youtube",
  "upload_link": null,
  "players_accepted": 2,
  "status_url": "?endpoint=stats-status&match_id=uuid",
  "poll_url": "?endpoint=stats-result&match_id=uuid",
  "csv_url": "?endpoint=stats-csv&match_id=uuid",
  "player_stats_url": "?endpoint=player-stats&match_id=uuid",
  "message": "Game created. When analysis is done we email the CSV, highlight links, and screenshot links."
}

Save match_id. You need it for the other endpoints. If upload_via_email was true, status is needs_upload. After footage is on the game, status is uploaded. After the game is confirmed, status is confirmed.

2. GET or POST ?endpoint=stats-status

Check the game you created. Identify it with match_id, key_code, or external_reference.

curl -X GET "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-status&match_id=MATCH_ID_FROM_STEP_1" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY"
statusMeaning
needs_uploadWaiting for the MP4. Response includes upload_link.
uploadedFootage is on the game. Drive/YouTube requests start here.
confirmedThe game is confirmed. Call stats-result until status is ready.

3. Ready email

There is no 24-hour cutoff. The email to notify_email includes a CSV of every player’s stats (player name first), each player’s highlight link and screenshot link, and the game highlight link. It does not include a Statbox join link, match_id, or key_code.

4. GET or POST ?endpoint=stats-result

Returns the specific game you requested. Identify it with match_id, key_code, or external_reference.

curl -X GET "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-result&match_id=MATCH_ID_FROM_STEP_1" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY"

Display the match only when status is ready. Before that, this call returns match_id, status, and a not-ready message. game_status: confirmed means the game is confirmed. That does not release scores or media. A player with no highlightable actions does not keep the match in processing_highlights.

FieldValues
game_statusneeds_upload, uploaded, confirmed. Same as stats-status.
statusunder_review, processing_highlights, ready, cancelled, pending_payment.

Football stats

Every value is an integer. A missing key means 0. There is no per-player rating. The match score is score. Do not rebuild it by summing goals.

KeyMeaning
goalsGoals scored
assistsAssists
shots_on_targetShots on target
shots_off_targetShots off target
chances_createdChances created for a teammate
successful_dribblesSuccessful dribbles
successful_tibSuccessful tackles, interceptions, and blocks
savesGoalkeeper saves
wrong_passMisplaced passes. A missing key means 0.

Padel stats

Keys are winner and ace, not winners or aces. There is no points or forced_errors field. Use score for the match result.

KeyMeaning
aceService aces
serveServe points won
returnReturn points won
winnerWinners
smashSmashes
lobLobs
dropshotDropshots
long_rallyLong rallies
double_faultsDouble faults
unforced_errorsUnforced errors

Basketball stats

Send sport: "basketball" and court_size of half or full. Every value is an integer. A missing key means 0. There is no points total. fg_made already includes made threes. Do not add three_pts_made on top of it. The turnover key is singular: turnover.

KeyMeaning
fg_madeField goals made, including three-pointers
fg_attemptedField goal attempts, including three-point attempts
three_pts_madeThree-pointers made
three_pts_attemptedThree-point attempts
free_throws_scoredFree throws made
free_throws_attemptedFree throw attempts
offensive_reboundOffensive rebounds
defensive_reboundDefensive rebounds
assistsAssists
blocksBlocks
turnoverTurnovers

Points, if you need them: 2 * (fg_made - three_pts_made) + 3 * three_pts_made + free_throws_scored. A completed basketball JSON example is in api.md.

Each player object also has name, team (A or B), jersey_number (integer or null), external_id (string or null, exact match), slot_number, highlight_url, and screenshot_url. stats is {} when nothing has been saved.

Example: completed football match

Illustrative values. These URLs are samples and do not resolve. A real match has one player object per person, up to 40, with the same keys. The full lineup example is in the Markdown reference.

{
  "match_id": "7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5",
  "key_code": "FB29KX",
  "join_url": "https://www.statbox.co/create/join?code=FB29KX",
  "external_reference": "BOOKING-123",
  "status": "ready",
  "game_status": "confirmed",
  "payment_status": "paid",
  "highlights_ready": true,
  "sport": "football",
  "date": "2026-10-12",
  "start_time": "18:00",
  "end_time": "19:00",
  "venue": { "id": null, "name": "Riverside Pitch" },
  "pitch": { "name": "Pitch 1", "slug": null },
  "score": { "team_a": 3, "team_b": 1 },
  "game_highlight_url": "https://d1nvampodjxeg6.cloudfront.net/match_clips/7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5/game/highlight.mp4",
  "players": [
    {
      "name": "Ahmed Khalil",
      "team": "A",
      "jersey_number": 9,
      "external_id": "player-88",
      "slot_number": 1,
      "stats": {
        "goals": 2, "assists": 0, "shots_on_target": 4, "shots_off_target": 1,
        "chances_created": 1, "successful_dribbles": 3, "successful_tib": 1,
        "saves": 0, "wrong_pass": 2
      },
      "highlight_url": "https://d1nvampodjxeg6.cloudfront.net/match_clips/7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5/1/highlight.mp4",
      "screenshot_url": "https://pfmdviiiwbpcutebhlkw.supabase.co/storage/v1/object/public/match_photos/temp_stats/example_slot_1.jpg"
    }
  ],
  "csv_endpoint": "?endpoint=stats-csv&match_id=7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5",
  "player_stats_endpoint": "?endpoint=player-stats&match_id=7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5"
}

Example: completed padel match

{
  "match_id": "9f1e8d70-4a2b-4c6d-8e90-112233445566",
  "key_code": "PD88QA",
  "status": "ready",
  "game_status": "confirmed",
  "highlights_ready": true,
  "sport": "padel",
  "date": "2026-10-12",
  "score": { "team_a": 6, "team_b": 4 },
  "game_highlight_url": "https://d1nvampodjxeg6.cloudfront.net/match_clips/9f1e8d70-4a2b-4c6d-8e90-112233445566/game/highlight.mp4",
  "players": [
    {
      "name": "Maya Nassar",
      "team": "A",
      "jersey_number": null,
      "external_id": "padel-user-1",
      "slot_number": 1,
      "stats": {
        "ace": 3, "serve": 8, "return": 5, "winner": 6, "smash": 2,
        "lob": 4, "dropshot": 1, "long_rally": 3, "double_faults": 1, "unforced_errors": 2
      },
      "highlight_url": "https://d1nvampodjxeg6.cloudfront.net/match_clips/9f1e8d70-4a2b-4c6d-8e90-112233445566/1/highlight.mp4",
      "screenshot_url": "https://pfmdviiiwbpcutebhlkw.supabase.co/storage/v1/object/public/match_photos/temp_stats/example_padel_1.jpg"
    }
  ]
}

A doubles match has four player objects. Each uses the same padel keys. The full four-player example is in api.md.

Example: results not ready

HTTP 200 from stats-result, player-stats, and stats-csv until the full package is ready. No scores, stats, CSV, screenshots, or highlight URLs.

{
  "match_id": "7c2a9e14-1b0d-4f3a-9c88-0a11b2c3d4e5",
  "status": "processing_highlights",
  "message": "Match results are not ready yet."
}

external_id is the player id in your app. Players do not need a Statbox account. A player with no highlightable actions does not keep the match processing. When ready, that player's highlight_url is null and stats are {} or zeros.

5. GET or POST ?endpoint=player-stats

One player from that same game. Send the game id plus one of external_id, slot_number, or player_name.

curl -X GET "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=player-stats&match_id=MATCH_ID_FROM_STEP_1&external_id=player-88" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY"

The body is match_id, key_code, join_url, status, sport, date, score, game_highlight_url, and player (the same player object as in stats-result). It does not return other players or that player’s other Statbox games.

If the external_id was never sent, or that player is not on the game, the response is HTTP 404:

{ "error": "Player not found in this game" }

6. GET or POST ?endpoint=stats-csv

Same match as a CSV file. Same data as the email attachment.

curl -X GET "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=stats-csv&match_id=MATCH_ID_FROM_STEP_1" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY" \
  -o statbox-stats.csv

GET or POST ?endpoint=games-left

How many games this key can still create this calendar month. This call does not create a game and does not use up the monthly cap.

curl -X GET "https://pfmdviiiwbpcutebhlkw.supabase.co/functions/v1/statbox-partner-api?endpoint=games-left" \
  -H "Authorization: Bearer sbx_live_YOUR_API_KEY"
{
  "used": 12,
  "limit": 100,
  "remaining": 88,
  "resets_at": "2026-11-01T00:00:00.000Z"
}

used is successful game creates since 00:00 UTC on the first of this month. remaining is limit - used. resets_at is 00:00 UTC on the first day of next month. An unlimited key returns "limit": null and "remaining": null. used is still the number created this month.

Errors

All errors are JSON: { "error": "message" }.

HTTPMeaning
401Bad or missing API key
403This key cannot use this product, or the key is disabled
400Missing or invalid fields (for example missing location or pitch)
404That game was not requested with this key, or that external_id is not on the game
409player_name matches more than one player. Use external_id or slot_number
429Too many requests this minute, or this key’s monthly game cap is reached

429 this minute

Wait 60 seconds, then retry the same call. Do not create another game. The default limit is 60 requests per key per rolling minute.

{ "error": "Rate limit exceeded. Try again in a minute." }

429 monthly cap

Returned by stats-request only. Unlimited keys do not return this. Call games-left to see used, limit, and remaining before you hit the cap. It resets at 00:00 UTC on the first day of the month.

{
  "error": "Monthly game limit reached (100/100). Try again next month.",
  "used": 100,
  "limit": 100
}

Media links

highlight_url and game_highlight_url are direct HTTPS URLs to MP4 files. Play them in a video player. They are not web pages. screenshot_url is a direct HTTPS JPEG or PNG. All of these URLs are public. Do not send the API key. They do not expire. Before status is ready, the API does not return stats or media. On a ready match, null means that file does not exist. A player with no highlightable actions has highlight_url null and still has stats.

Webhook: match.ready

Send webhook_url on stats-request. HTTPS port 443 only. The 201 response includes webhook_secret. Store it. It is not the API key. When the full match is ready, Statbox POSTs one match.ready event. The body includes event_id, event, timestamp, match_id, external_reference, and result. Retries keep the same event_id and the same raw body. Each attempt sends a new X-Statbox-Timestamp and a new HMAC. Reject a timestamp more than five minutes from your clock, and reject a signature that is missing or not 64 hex characters before you compare it. The full create example and receiver are in /docs/api.md. There is no separate stats.ready or highlights.ready event.

What this key can read

A key returns only games created with that key. There is no list endpoint, no way to fetch an existing Statbox game by a public code, and no way to download a player’s other Statbox matches.

Integration notes

  1. Store the API key as a server-side secret. Do not put it in a mobile app binary.
  2. Call stats-request once per game. Save match_id. Send external_id for every player you will show later.
  3. Call stats-status with that match_id until confirmed, then stats-result until ready.
  4. Call stats-result until status is ready. Until then the body has no scores, stats, or media. Polling still works if you do not set webhook_url.
  5. Call games-left to see how many games this key can still create. On a per-minute 429, wait 60 seconds and retry. On a monthly-cap 429, wait until next month before creating another game.
  6. Play highlight URLs as MP4 files. Do not attach the API key. They do not expire.
  7. This key cannot read games it did not create, and it cannot read a player’s other Statbox matches.