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`.
Related guides
- Settings — Create and revoke API keys
- Players — Player fields and management
- Events — Event fields and workflow
- Teams — Organizer-owned invite rosters
- Tournaments — Group events and aggregated results
- Invitations & RSVPs — How invitations work in the app
- Credits & Notifications — Credit costs for SMS/WhatsApp