Playbox Organizer API

Read data about your own games, leagues and matches from your script or server: who played and how they paid, tables, top scorers, goals, cards and highlights. Read-only, your data only.

Base URL: https://app.play-box.xyz/api/organizer/v1/

First request in 5 minutes

  1. Get a key. In the Playbox app open Settings → Organizer tools → API keys → Create key. Give the key a name and accept the terms. The key looks like pbo_xxxxxxxx.long-secret and is shown only once. Store it in your server’s environment variables.
  2. Check the connection.
curl -H "X-Api-Key: $PLAYBOX_KEY" https://app.play-box.xyz/api/organizer/v1/me/

A 200 response returns your username, the key name and the date the key is valid until.

  1. Fetch your games.
curl -H "X-Api-Key: $PLAYBOX_KEY" "https://app.play-box.xyz/api/organizer/v1/events/?from=2026-09-01&status=FINISHED"

Key

What counts as your data

Someone else’s game or league returns 404, the same as one that does not exist. Player contacts and photos are never returned.

Limits

60 requests per minute per key. A request is one call to any API URL with a valid key, including 404 and 400 responses. The window resets at the start of every minute.

Response headerMeaning
X-RateLimit-Limitlimit per minute
X-RateLimit-Remainingrequests left in the current minute
X-RateLimit-Resetseconds until the window resets
Retry-Afteronly on 429: seconds to wait before retrying

Store the data on your side and fetch only what is new: recent games by from and matches of finished games. Re-downloading the whole history every time your site opens will hit the limit within seconds.

Errors

Every error has the same shape. Branch on code, write message to your log.

{"error": {"code": "invalid_parameter", "message": "from: Use YYYY-MM-DD.", "fields": {"from": "Use YYYY-MM-DD."}}}
HTTPcodeWhat to do
401not_authenticatedthe key header is missing
401invalid_keythe key is wrong or revoked, create a new one
401key_expiredthe key has expired, create a new one
403browser_not_allowedthe request came from a browser, call the API from your server
404not_foundthe object does not exist or is not yours
400invalid_parametera parameter is wrong, details in fields
405method_not_allowedthe API is read-only, use GET
429rate_limitedwait retry_after seconds

Data format

Endpoints

GET /me/

Whose key this is: user, key name, expiry date, terms version, limits.

GET /leagues/

Your leagues: id, slug, name, status (active, finished, archived), format, city, date_start, date_end, teams_count, price, game_price, url.

GET /leagues/{id}/

The league and its rules: technical defeat, discipline (yellow card limit, suspensions), squad size.

GET /leagues/{id}/table/

The standings in the same order as in the app: position, team, played, wins, draws, losses, goals_for, goals_against, goal_difference, points.

GET /leagues/{id}/players/

Player table: player, team, position, games, goals, assists, yellow_cards, red_cards, suspension_games_left. Sorted by goals, then assists.

GET /leagues/{id}/rosters/

Team squads: each team with its players and the date they were added.

GET /events/

Your games, newest first. Filters: from, to (YYYY-MM-DD by start date, UTC), status (NOT_STARTED, STARTED, FINISHED, CANCELLED), league (id), format (players per team, for example 8). Fields: id, league_id, starts_at, ends_at, status, results_status, format, venue, is_paid, price, price_per_player, capacity, players_count, teams_count, is_ranked, is_private, url.

GET /events/{id}/

The whole game: teams (colour, wins, draws, losses, squad), matches with score and url, mvp_player_id, YouTube recordings, live streams.

GET /events/{id}/participants/

Who signed up and how they paid: player, status (In game, Late, Did not come, Invited), team_id, joined_at, payment_method (CASH, WALLET, CARD, STRIPE, APPLE_PAY, GOOGLE_PAY, TELEGRAM), paid, payment_confirmed, paid_by, amount, is_early_bird. This data is for you only: do not publish it.

GET /events/{id}/player-stats/

Each player’s result for the game: wins, draws, losses, goals, assists, goals_conceded, yellow_cards, red_cards, rating, rating_change, is_confirmed. Available after the game is finished.

GET /matches/{id}/

The whole match: event_id, league_id, number, tour, starts_at, ends_at, state, teams, score, technical_defeat, referee, video_url, url, the timeline, the penalty_shootout, lineups (who started, seconds on the pitch) and highlights (as YouTube links).

"timeline": [
  {"type": "period", "half": 1, "is_extra_time": false, "started_at": "...", "ended_at": "...", "duration_seconds": 1200},
  {"type": "goal", "game_time_seconds": 305, "team_id": 1, "player": {}, "assistant": {}, "is_own_goal": false, "youtube_url": "..."},
  {"type": "card", "game_time_seconds": 700, "team_id": 2, "player": {}, "card": "YELLOW", "reason": "Dissent"},
  {"type": "substitution", "game_time_seconds": 900, "team_id": 2, "player_out": {}, "player_in": {}, "until_end": true}
]

Examples

Python

import os, requests

API = "https://app.play-box.xyz/api/organizer/v1"
session = requests.Session()
session.headers["X-Api-Key"] = os.environ["PLAYBOX_KEY"]

def fetch_all(path, **params):
    offset = 0
    while offset is not None:
        page = session.get(f"{API}/{path}", params={**params, "offset": offset, "limit": 100}).json()
        yield from page["results"]
        offset = page["next_offset"]

for event in fetch_all("events/", status="FINISHED", **{"from": "2026-09-01"}):
    detail = session.get(f"{API}/events/{event['id']}/").json()
    for match in detail["matches"]:
        print(event["starts_at"], match["score"], match["url"])

JavaScript (Node 18+, server side)

const API = "https://app.play-box.xyz/api/organizer/v1";
const headers = { "X-Api-Key": process.env.PLAYBOX_KEY };

const res = await fetch(`${API}/leagues/8/players/`, { headers });
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
const { results } = await res.json();
console.log(results.slice(0, 10).map(r => `${r.player.username}: ${r.goals}`));

Terms of use (version 2026-10-01)

Versions

The version is in the URL (v1). New fields may appear in responses without a version change, so do not fail on unknown fields. Removing or renaming a field will happen only in v2, and v1 will keep working.