Public API


Use the PlayerPing public API to manage players, events, clubs, teams, tournaments, and player-in-event RSVPs from your own tools and scripts. Authenticate with an API key created in Settings.


Creating an API key


  • Sign in to PlayerPing and open Settings
  • In the API Keys section, enter a name (e.g. `Zapier`) and click Create API key
  • Copy the key immediately — it is shown only once
  • Store it securely (password manager, secrets vault, environment variable)

You can have up to 10 active keys. Revoke a key anytime from Settings; revoked keys stop working immediately.


Authentication


Send your API key as a Bearer token:


Authorization: Bearer pp_live_YOUR_SECRET_KEY


All public endpoints live under `/api/v1`. Session cookies are not accepted on these routes.


Base URL


https://playerping.me/api/v1


Endpoints


Account & credits


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/me` | Account snapshot: `id`, `email`, `name`, `credits`, `smsEnabled`, `emailEnabled` |

| `GET` | `/credits` | Credit balance + notification flags (preflight before `notify: true`) |


Use these before sending invitations. SMS/WhatsApp cost 1 credit each when SMS is enabled; email is free. Insufficient credits return HTTP `402` on send.


Players


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/players` | List your players (includes average rating) |

| `GET` | `/players/stats` | Attendance aggregates per player (Invited / Attended / Declined / Rate / streak) |

| `POST` | `/players` | Create a player |

| `GET` | `/players/:id` | Get one player |

| `PATCH` | `/players/:id` | Update a player |

| `DELETE` | `/players/:id` | Delete a player |


Create / update body:


{

"name": "Alex Rivera",

"phone": "+15551234567",

"gender": "M",

"sport": "Tennis",

"email": "alex@example.com",

"clubId": "optional-club-id"

}


`name`, `phone`, `gender`, and `sport` are required. Gender must be `M`, `F`, or `OTHER`. Phone must be international format (e.g. `+15551234567`).


On `PATCH`, omit `email` or `clubId` to leave the existing values unchanged. Send `null` (or an empty string) to clear them.


World Tennis Number (tennis players): Player responses include optional WTN fields when linked:


  • `wtnTennisId` — ITF person id (canonical global key)
  • `wtnSingles`, `wtnDoubles` — cached ratings as JSON numbers (1 = pro, 40 = beginner; lower is stronger)
  • `wtnProfileUrl` — link to the ITF profile
  • `tnzNationalId`, `wtnDirectoryClubName` — optional metadata when linked via Tennis NZ MatchPoint
  • `wtnUpdatedAt` — ISO timestamp of last rating sync

These same fields appear on nested `player` objects in event responses (`GET /events/:id`, `GET /events/:id/players`).


On PATCH, you may set `wtnTennisId` or `wtnProfileUrl` to an ITF profile URL to link a player. Ratings are resolved server-side (not set directly). Set either field to `null` to unlink. Linking is only allowed when `sport` is `Tennis`.


WTN linking in the browser UI (search, confirm, refresh) uses session routes under `/api/players/...` and is not part of the public v1 API.


Attendance stats (`GET /players/stats`): returns `{ "stats": [...] }` with `playerId`, `playerName`, `eventsInvited`, `eventsAttended`, `eventsDeclined`, `eventsPending`, `attendanceRate`, `currentStreak`, and `lastEventDate`. Organizer `UNAVAILABLE` rows are excluded from Invited / Declined / Rate / streak (same as the app).


Clubs


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/clubs` | List public platform clubs and your private clubs (`?sport=` optional) |

| `POST` | `/clubs` | Create a private club (visible only to you) |


Create body:


{

"name": "Tuesday Night Crew",

"sport": "Tennis",

"location": "Central Courts",

"countryCode": "NZ",

"city": "Auckland"

}


`name` and `sport` are required. `location`, `countryCode`, and `city` are optional.


Clubs created via the API are private to your account. Platform-wide public clubs (maintained by admins) are listed but cannot be created via the API. Use a returned `clubId` when creating or updating players and events.


Teams


Teams are organizer-owned invite rosters. Members can come from different clubs. A player may belong to more than one team.


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/teams` | List your teams (`?sport=` optional) |

| `POST` | `/teams` | Create a team |

| `GET` | `/teams/:id` | Get one team with members |

| `PATCH` | `/teams/:id` | Update name, sport, or members |

| `DELETE` | `/teams/:id` | Delete a team (events keep running with `teamId` cleared) |


Create / update body:


{

"name": "Saturday Mixed",

"sport": "Tennis",

"members": [

{ "playerId": "player_cuid_1", "kind": "BASE", "rank": 1 },

{ "playerId": "player_cuid_2", "kind": "RING_IN", "rank": 2 }

]

}


`name` and `sport` are required on create. Team names must be unique on your account.


`kind` is `BASE` or `RING_IN` (defaults to `BASE`). `rank` is a non-negative integer (defaults to array order). Classification is per team: the same player can be Base on one roster and Ring-in on another.


On `PATCH`, omit `members` and `playerIds` to leave membership unchanged. Send `members` as a full replace set. `playerIds` still works as a convenience: listed players stay on the roster, remaining players keep their existing kind, new players default to `BASE`, and rank follows array order. If both are sent, `members` wins. Players must be yours and match the team sport.


Member objects in `GET` responses include `kind` and `rank`. Event payloads that include `team.members` include those fields too.


Tournaments


Tournaments group events for combined results. Events stay independent unless you set `tournamentId`. A team can play events in different tournaments, or in none.


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/tournaments` | List your tournaments (`?sport=` optional) |

| `POST` | `/tournaments` | Create a tournament |

| `GET` | `/tournaments/:id` | Get one tournament with events and aggregated `results` |

| `PATCH` | `/tournaments/:id` | Update name, sport, or description |

| `DELETE` | `/tournaments/:id` | Delete a tournament (events keep running with `tournamentId` cleared) |


Create / update body:


{

"name": "Spring Cup",

"sport": "Tennis",

"description": "Saturday mixed league"

}


`name` and `sport` are required on create. Tournament names must be unique on your account. `description` is optional; send `null` on PATCH to clear it.


`GET /tournaments/:id` includes:


  • `events` — attached events (no RSVP tokens) with `responseCount`
  • `results` — totals across those events: event counts, unique players/teams, RSVP counts, attendance rate, average rating, plus per-player and per-team rows

Events


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/events` | List non-archived events (`?archived=true` to include archived) |

| `POST` | `/events` | Create an event |

| `GET` | `/events/:id` | Get event with responses (tokens stripped) |

| `PATCH` | `/events/:id` | Update an event |

| `DELETE` | `/events/:id` | Delete an event and its responses |


Create body:


{

"date": "2026-09-01",

"time": "18:00",

"location": "Central Courts",

"sport": "Tennis",

"requiredPlayers": { "M": 2, "F": 2 },

"allowProposeTime": false,

"clubId": null,

"teamId": null,

"tournamentId": null

}


Optional `recurrence`: `{ "frequency": "weekly"|"biweekly"|"monthly", "endDate": "ISO", "count": N }`.


Optional `teamId` must be a team you own whose sport matches the event. Event responses include `teamId` and a nested `team` (id, name, sport, members with `playerId`, `kind`, and `rank`). When `teamId` is set, adding players with `POST /events/:id/players` is limited to that roster. Club broadcast is unchanged.


Optional `tournamentId` must be a tournament you own whose sport matches the event. Event responses include `tournamentId` and a nested `tournament` (id, name, sport). Leave it empty for a standalone event. Recurring series copy the tournament onto every occurrence.


Patch fields (partial update): `date`, `time`, `location`, `sport`, `status` (`OPEN` | `PENDING` | `CONFIRMED` | `CANCELLED`), `requiredPlayers`, `allowProposeTime`, `archived` (past events only), `clubId`, `teamId`, `tournamentId`.


Players on an event (RSVPs)


| Method | Path | Description |

|--------|------|-------------|

| `GET` | `/events/:id/players` | List responses for the event |

| `POST` | `/events/:id/players` | Add players to the event |

| `PATCH` | `/events/:id/players/:playerId` | Update RSVP status |

| `DELETE` | `/events/:id/players/:playerId` | Remove player from event |

| `POST` | `/events/:id/waitlist/:responseId/promote` | Promote waitlisted player to YES |


Add players body:


{

"playerIds": ["player_cuid_1", "player_cuid_2"],

"notify": false,

"status": "PENDING"

}


  • By default (`notify: false`), responses are created silently (no SMS/email, no credit charge).
  • Set `notify: true` to send invitations using your notification settings. SMS/WhatsApp cost credits (same as the app); email is free. Insufficient credits return HTTP `402`.
  • `status` is only allowed when `notify` is false. Valid values: `PENDING`, `YES`, `NO`, `MAYBE`, `WAITLIST`.
  • To pre-mark a player unavailable without inviting: `{ "playerIds": ["…"], "notify": false, "status": "UNAVAILABLE" }` (also available in the app via Mark Unavailable). This status is excluded from Attendance Stats Invited/Declined/Rate.
  • Successful add responses include `responses`, plus `notificationsSent`, `creditsUsed`, and `creditsRemaining` when notifications were attempted.

Update status body: `{ "status": "YES" }`


Promote a waitlisted response with `POST /events/:id/waitlist/:responseId/promote` (no body). The response id is the RSVP row id from `GET /events/:id/players`, not the player id.


Fill this event


Shared plan/verify engine with the in-app Fill this event harness. Plan never spends credits; send only after explicit approval.


| Method | Path | Description |

|--------|------|-------------|

| `POST` | `/events/:id/fill/plan` | Return a `FillPlan` (gaps, ordered invite steps, credit estimate) — never sends |

| `POST` | `/events/:id/fill/execute` | Send invites only when `approved: true` |

| `POST` | `/events/:id/fill/verify` | Deterministic verifier checks against the current board |


Plan body: `{ "additionalInfo"?: string, "useAi"?: boolean }` (default `useAi: true` when AI is configured). Response: `{ plan, insufficientCredits, canAfford }`. Rate-limited (`429`).


Execute body: `{ "playerIds": [...], "approved": true, "planId"?: "..." }`. Requests without `approved: true` are refused (`400`). Optional `planId` must match the plan’s id for the selected `playerIds`. Double-submit of the same plan within a short window returns `409`. Equivalent send path: `POST /events/:id/players` with `notify: true`.


Verify body (optional): `{ invitedPlayerIds?, notify?, creditsBefore?, creditsUsed?, creditsAfter?, creditsEstimate? }` → `{ result: { ok, checks, gapsRemaining, rosterFull } }`.


Example: list players


curl -sS https://playerping.me/api/v1/players \

-H "Authorization: Bearer pp_live_YOUR_SECRET_KEY"


Example: create event and invite players


Create event

curl -sS -X POST https://playerping.me/api/v1/events \

-H "Authorization: Bearer pp_live_YOUR_SECRET_KEY" \

-H "Content-Type: application/json" \

-d '{

"date": "2026-09-01",

"time": "18:00",

"location": "Central Courts",

"sport": "Tennis",

"requiredPlayers": { "M": 2, "F": 2 }

}'


Add players without notifying

curl -sS -X POST https://playerping.me/api/v1/events/EVENT_ID/players \

-H "Authorization: Bearer pp_live_YOUR_SECRET_KEY" \

-H "Content-Type: application/json" \

-d '{ "playerIds": ["PLAYER_ID"], "notify": false }'


Errors


Responses use `{ "error": "message" }` with standard HTTP status codes:


  • `401` — Missing or invalid API key
  • `403` — Resource belongs to another user
  • `404` — Not found
  • `402` — Insufficient credits (when `notify: true` / fill execute)
  • `400` — Validation error (including execute without `approved: true`)
  • `429` — Rate limited (fill plan / execute / verify)
  • `500` — Server error

Security


  • Treat API keys like passwords. Never commit them to source control or share them in chat.
  • Revoke compromised keys immediately in Settings.
  • Changing your account password revokes all API keys and ends other sessions — create new keys afterward.
  • Magic-link RSVP tokens are never returned by the public API.
  • API keys cannot perform admin actions.
  • Fill plan may use server-side AI ranking (rate-limited). Fill execute requires `approved: true` and never auto-sends.
  • Ratings write, chat, time proposals, club/team broadcasts, and Stripe purchases are not part of the public v1 API (use the web app / session APIs).

Agent skill (for LLMs)


Public URL (no registration required):


https://playerping.me/skills/playerping-api/SKILL.md


Human-readable page: Agent Skill


Point any LLM or agent at that URL (or attach the Markdown). Provide your API key via a secret or environment variable (`PLAYERPING_API_KEY`), not in the skill file. The skill covers auth, endpoints, safety defaults (`notify: false`), credit preflight (`GET /credits` / `GET /me`), and a fill an event loop (`fill/plan` → human confirms → `fill/execute` or `notify: true`).


Repo copies (for contributors): `skills/playerping-api/SKILL.md`, `.cursor/skills/playerping-api/SKILL.md`.