Pitch Connect API
Base URL: https://pitch-connect.space
Club data (players, games) is read-only. Match scores, goals, and event markers are write-enabled.
Overview
The Pitch Connect API lets third-party apps read club squad data and upcoming fixtures, write match scores and goal scorers, and record granular match event markers (goals, shots, saves, cards, substitutions, and more) for video analysis and live match tracking.
Two token types work interchangeably:
| Type | Description | Where to get it |
|---|---|---|
| Per-app token | Generated when a user connects your app via OAuth. Each connection gets its own independent token — revoke one without affecting others. | OAuth flow below |
| Static key | A single shared key for the club. Good for scripts and direct integrations. | Club → Integrations page |
OAuth-style Club Connection
Use this when you're building an app and want your users to connect their Pitch Connect club without manually copying API keys.
Full flow
1. Your app → redirect user to:
GET https://pitch-connect.space/api/auth/connect
?app_name=MyApp
&redirect_uri=https://myapp.com/callback
2. User logs in to Pitch Connect (if not already signed in)
A focused consent screen is shown — no app nav or footer.
3. User sees your app name and their eligible clubs.
They click the club they want to connect.
4. Pitch Connect → redirects user back to your app with a
fresh per-app token unique to this connection:
GET https://myapp.com/callback
?api_key=a1b2c3d4...
&club_name=Westminster+FC
&club_slug=westminster-fc
Step 1 — Redirect the user
GET https://pitch-connect.space/api/auth/connect?app_name=MyApp&redirect_uri=https://myapp.com/callback
| Parameter | Required | Description |
|---|---|---|
app_name |
Yes | Your app's name, shown on the consent screen |
redirect_uri |
Yes | Where to send the user after connecting. Must be https:// in production (http://localhost allowed in development). Cannot point to pitch-connect.space. |
Step 2 — Handle the callback
After the user selects a club, Pitch Connect redirects to your redirect_uri with:
| Parameter | Type | Description |
|---|---|---|
api_key |
string | A fresh per-app token — unique to this connection. Store securely. |
club_name |
string | Human-readable club name |
club_slug |
string | URL slug for the club |
Example:
https://myapp.com/callback?api_key=a1b2c3d4...&club_name=Westminster+FC&club_slug=westminster-fc
Who can connect a club
| Role | Can connect? |
|---|---|
| Club owner | Yes |
| Club moderator | Yes |
| Club member only | No |
| Pitch Connect admin | Yes (any club) |
Re-connecting
Each OAuth connection creates a new per-app token. Previous tokens from earlier connections continue working until revoked. This lets a user reconnect (e.g. after revoking) without breaking other integrations.
Revoking a connection
Club owners can see all connected apps and revoke individual tokens from:
https://pitch-connect.space/clubs/{your-club-slug}/integrations
Revoking one app does not affect other connected apps or the static key.
Manual Key Generation
For scripts and direct integrations — a single shared key for the club.
- Go to your club page on Pitch Connect
- Click Manage → next to the API & Integrations summary in the sidebar
— or navigate directly to
/clubs/{your-club-slug}/integrations - Under Static API Key, click Generate Static Key
- Copy the key — treat it like a password
From the same page: Regenerate (rotates key, old one stops working immediately) or Revoke (removes access entirely).
Managing Integrations
The Integrations page (/clubs/{your-club-slug}/integrations) is the central hub for API access:
- Connected Apps — lists every OAuth-connected app with its name, connection date, last-used time, and a Disconnect button to revoke that token
- Static API Key — generate, regenerate, or revoke the shared key
Making API Requests
All endpoints are under /api/v1/ and require a token as a Bearer header.
Authorization: Bearer <your_api_key>
The token identifies which club's data is being accessed — no slug needed in the URL.
Endpoints
GET /api/v1/club
Returns the full club profile: details, all active players, and next 20 upcoming games in one call.
curl https://pitch-connect.space/api/v1/club \
-H "Authorization: Bearer <your_api_key>"
Response
json
{
"id": 12,
"name": "Westminster FC",
"slug": "westminster-fc",
"city": "London",
"location": "Westminster, London",
"description": "Sunday league side, all levels welcome.",
"member_count": 18,
"is_private": false,
"created_at": "2024-09-01T10:00:00Z",
"badge_url": "https://pitch-connect.space/rails/active_storage/...jpg",
"cover_photo_url": "https://pitch-connect.space/rails/active_storage/...jpg",
"players": [ ... ],
"upcoming_games": [ ... ]
}
| Field | Type | Notes |
|---|---|---|
badge_url |
string \ | null |
cover_photo_url |
string \ | null |
GET /api/v1/club/players
Active squad members with jersey numbers, positions, and skill levels.
Jersey numbers and club positions are set by club staff in the app (Members page) and read here.
curl https://pitch-connect.space/api/v1/club/players \
-H "Authorization: Bearer <your_api_key>"
Response
json
{
"club": "Westminster FC",
"players": [
{
"id": 42,
"slug": "alex-johnson",
"name": "Alex Johnson",
"jersey_number": 10,
"club_position": "midfielder",
"effective_position": "midfielder",
"preferred_position": "forward",
"secondary_positions": ["defender"],
"skill_level": "intermediate",
"role": "Owner",
"joined_at": "2024-09-01T10:00:00Z",
"profile_url": "https://pitch-connect.space/players/alex-johnson"
}
]
}
Player fields
| Field | Type | Notes |
|---|---|---|
id |
integer | User ID — use this to match players in game team lists |
slug |
string | URL-friendly identifier |
name |
string | Full display name |
jersey_number |
integer | Squad number (1–99). Omitted if not assigned |
club_position |
string | Position assigned by the club: goalkeeper · defender · midfielder · forward. Omitted if not set |
effective_position |
string | club_position if set, otherwise preferred_position. Use this for lineup and team formation |
preferred_position |
string | Player's own preferred position |
secondary_positions |
string[] | Other positions the player covers. May be empty |
skill_level |
string | beginner · intermediate · advanced |
role |
string | Owner · Moderator · Member |
joined_at |
ISO 8601 | UTC timestamp when they joined the club |
profile_url |
string | Link to their Pitch Connect profile |
GET /api/v1/club/upcoming_games
Next 20 games ordered by kick-off time with full team breakdowns and match pairings.
curl https://pitch-connect.space/api/v1/club/upcoming_games \
-H "Authorization: Bearer <your_api_key>"
Response
json
{
"club": "Westminster FC",
"games": [
{
"id": 301,
"slug": "westminster-sunday-kickabout",
"title": "Westminster Sunday Kickabout",
"matchup": "Reds vs Blues · Greens vs Yellows",
"date_time": "2026-06-01T10:00:00Z",
"timezone": "London",
"location": "Battersea Park, London",
"field": "Pitch 3",
"pitch": "5-a-side",
"description": "Friendly match, bring water.",
"notes": "Gates open 15 mins before kick-off.",
"max_players": 10,
"players_joined": 7,
"spots_left": 3,
"skill_level": "intermediate",
"cost_per_player": 5.0,
"is_public": true,
"organizer": { "id": 1, "name": "Alex Johnson" },
"teams": [
{
"number": 1,
"name": "Reds",
"players": [
{
"id": 42,
"name": "Alex Johnson",
"is_guest": false,
"is_captain": true,
"is_substitute": false,
"pitch_role": "goalkeeper",
"checked_in": true
}
]
}
],
"matchups": [
{ "match_id": "301_0", "match_index": 0, "home_team": 1, "home_name": "Reds", "away_team": 2, "away_name": "Blues" },
{ "match_id": "301_1", "match_index": 1, "home_team": 3, "home_name": "Greens", "away_team": 4, "away_name": "Yellows" }
],
"unassigned_players": [],
"score": "Reds 2 · Blues 1",
"url": "https://pitch-connect.space/games/westminster-sunday-kickabout"
}
]
}
Game fields
| Field | Type | Notes |
|---|---|---|
matchup |
string | All pairings in one string: "Reds vs Blues · Greens vs Yellows". Bye shown as "Team X (bye)" |
date_time |
ISO 8601 | Kick-off time in UTC |
timezone |
string | Timezone the game was scheduled in |
location |
string \ | null |
field |
string \ | null |
pitch |
string \ | null |
description |
string \ | null |
notes |
string \ | null |
max_players |
integer | Total player capacity |
players_joined |
integer | Confirmed players |
spots_left |
integer | max_players − players_joined |
skill_level |
string \ | null |
cost_per_player |
number \ | null |
organizer |
object | { id, name } |
teams |
array | See team object below |
matchups |
array | [{ home_team, home_name, away_team, away_name }]. away_* are null for a bye |
unassigned_players |
array | Confirmed players not yet on a team |
score |
string \ | null |
url |
string | Link to the game on Pitch Connect |
Team object
| Field | Type | Notes |
|---|---|---|
number |
integer | Team number |
name |
string | Custom name or "Team N" |
players |
array | Confirmed players on this team |
Player-in-game object
| Field | Type | Notes |
|---|---|---|
id |
integer \ | null |
name |
string | Display name |
is_guest |
boolean | true if not a registered user |
is_captain |
boolean | Team captain |
is_substitute |
boolean | On the bench |
pitch_role |
string \ | null |
checked_in |
boolean | Whether they checked in |
PATCH /api/v1/games/:slug/scores
Update the score for each team. Only works for games belonging to the authenticated club.
curl -X PATCH https://pitch-connect.space/api/v1/games/thursday-kickabout/scores \
-H "Authorization: Bearer <your_api_key>" \
-H "Content-Type: application/json" \
-d '{ "scores": { "1": 2, "2": 1 } }'
| Field | Type | Description |
|---|---|---|
scores |
object | Map of team number (string key) to score (integer). E.g. { "1": 2, "2": 0, "3": 1 } |
Response
json
{
"message": "Scores updated.",
"game": {
"id": 301,
"slug": "thursday-kickabout",
"title": "Thursday Kickabout",
"team_scores": { "1": 2, "2": 1 },
"score": "Reds 2 · Blues 1",
"goals": [ ... ]
}
}
PATCH /api/v1/games/:slug/goals
Replace the full goal list. Sends the complete array — overwrites all existing goals.
curl -X PATCH https://pitch-connect.space/api/v1/games/thursday-kickabout/goals \
-H "Authorization: Bearer <your_api_key>" \
-H "Content-Type: application/json" \
-d '{
"goals": [
{
"team": "1",
"scorer": "Alex Johnson",
"user_id": 42,
"minute": "23",
"penalty": false,
"own_goal": false,
"assister": "Sam Smith",
"assister_user_id": 15
}
]
}'
Goal object
| Field | Required | Notes |
|---|---|---|
team |
Yes | Team number as string: "1", "2", etc. |
scorer |
Yes | Scorer display name |
user_id |
No | Scorer's user ID from /players. Omit for guests |
minute |
No | Minute of goal, e.g. "45" |
penalty |
No | true if penalty |
own_goal |
No | true if own goal |
assister |
No | Assister display name |
assister_user_id |
No | Assister user ID from /players |
Note: Scores and goals are independent. Call both endpoints to keep them in sync, or derive scores by counting goals in your app.
POST /api/v1/games/:slug/marker
Record a single match event marker. Idempotent — sending the same id again updates the existing marker rather than creating a duplicate.
The game must belong to the club identified by your API token.
curl -X POST https://pitch-connect.space/api/v1/games/thursday-kickabout/marker \
-H "Authorization: Bearer <your_api_key>" \
-H "Content-Type: application/json" \
-d '{
"id": "e305e94b-4b10-4bc3-be9d-472d2fb2d2e1",
"timestamp": "2026-06-02T14:15:22Z",
"type": "Save",
"primary_player_pc_id": "8432",
"secondary_player_pc_id": "9021",
"label": "Tipped over crossbar"
}'
Request body
| Field | Required | Description |
|---|---|---|
id |
Yes | Client-generated UUID — prevents duplicate markers (upsert key) |
match_id |
Yes | The match_id from the matchup in /club/upcoming_games. Format: {game_id}_{match_index} e.g. 301_0. Required when the game has multiple match pairings. |
timestamp |
Yes | ISO-8601 datetime of the event |
type |
Yes | Event type — see allowed values below |
primary_player_pc_id |
No | User ID of the primary player (scorer, shooter, outgoing sub…) |
secondary_player_pc_id |
No | User ID of the secondary player (assister, chance creator, incoming sub…) |
label |
No | Optional free-text note |
Allowed type values
| Category | Values |
|---|---|
| Match lifecycle | Start · Break · Resume · End · PenaltyShootout · ResetMatch |
| Match events | Goal · Miss · Save · Shot · Foul · YellowCard · RedCard · Substitution · Penalty · PenaltyScored · PenaltyMissed · Event |
Player field mapping by type
| Type | primary_player_pc_id |
secondary_player_pc_id |
|---|---|---|
Goal |
Scorer | Assister |
Shot / Miss / Save |
Shooter | Chance creator |
Substitution |
Outgoing player | Incoming player |
Foul |
Foul committer | Fouled player |
Penalty |
Fouled player | Foul committer |
PenaltyScored |
Penalty taker | — |
PenaltyMissed |
GK (if saved) / Taker (if missed) | Taker (if saved) / — |
YellowCard / RedCard |
Booked player | — |
PenaltyShootout |
— | — |
ResetMatch |
— | — |
Start / Break / Resume / End / Event |
— | — |
Response
json
{ "status": "success", "marker_id": "e305e94b-4b10-4bc3-be9d-472d2fb2d2e1" }
Returns 201 Created for new markers, 200 OK for updates.
GET /api/v1/games/:slug/markers
Returns all markers for a game ordered by timestamp.
curl https://pitch-connect.space/api/v1/games/thursday-kickabout/markers \
-H "Authorization: Bearer <your_api_key>"
Response
json
{
"game": { "id": 301, "slug": "thursday-kickabout" },
"game_mode": false,
"markers": [
{
"id": "e305e94b-...",
"timestamp": "2026-06-02T14:15:22Z",
"type": "Save",
"primary_player_pc_id": "8432",
"secondary_player_pc_id": "9021",
"label": "Tipped over crossbar",
"primary_player": { "id": 8432, "name": "Alex Johnson" },
"secondary_player": { "id": 9021, "name": "Sam Smith" }
}
]
}
game_mode: true means the game is in Game Mode On — only Start, Goal, YellowCard, RedCard, Substitution, Break, Resume, PenaltyScored, PenaltyMissed, PenaltyShootout, ResetMatch will be present. The base Penalty marker (with fouler/fouled details) is not transmitted in game mode.
Error Responses
| HTTP Status | Meaning |
|---|---|
401 Unauthorized |
Missing or invalid token |
404 Not Found |
Resource not found or doesn't belong to this club |
422 Unprocessable |
Invalid request body |
401 example
json
{
"error": "Invalid API token.",
"hint": "Get a key from your club's Integrations page or connect via OAuth."
}
Notes
- All timestamps are UTC ISO 8601
GET /api/v1/clubreturns players + games in one call — use the focused endpoints if you only need one- Only active members appear in the players list — pending and former members are excluded
- Jersey numbers and club positions are set in the app by club staff (Members page) and read via the API
- Use
effective_positionfor lineup/formation — it returnsclub_positionif set, otherwise falls back topreferred_position - Games are capped at 20, ordered by kick-off time ascending
- Updating goals does not auto-update
team_scores— call both endpoints or count goals in your app - Event markers (
POST /game/:slug/marker) are idempotent — send the same clientidto update without creating a duplicate - When
game_modeistrue, only simplified types are sent:Start,Goal,YellowCard,RedCard,Substitution,Break,Resume,PenaltyScored,PenaltyMissed,PenaltyShootout,ResetMatch— the structuralPenaltymarker is not transmitted in game mode primary_player_pc_id/secondary_player_pc_idare the player'sidfrom the/playersendpoint- Write endpoints only accept games belonging to the club identified by the token
- Per-app tokens can be revoked individually from
/clubs/{your-club-slug}/integrationswithout affecting other connections