$WHISTLE_API_KEY and the production base URL:
externalId is your stable id for the record. Together with source — your system’s namespace, e.g. "huddleup" — it forms the idempotency key: re-posting the same pair updates instead of duplicating.
You don’t set
source — your key does. Writes are stamped with the source your key is bound to. Omit the field (as these examples do) or echo your own value; any other value returns 400 {"error":"source_mismatch"}. See what your key can access.Import or upsert leagues and games
Inputs: your league’s name and id, plus each game’s start time (ISO 8601), pay (dollars), and your game id.Upsert the league
201 {"data": {"id": "…", "name": "Springfield Little League", …}}. Save data.id — it’s the leagueId for every later call.Idempotency: re-running updates name, paymentMode, and currency on the existing league (still 201). No duplicate is created.Batch-import games (1–500 per request)
201 {"data": {"created": [ …Game objects with positions… ]}}. Each created game has one position (your label, your pay).Idempotency: re-posting a game with the same (source, externalId) updates its sport, startsAt, location, and coordinates — but never touches positions, pay, or existing claims, so a re-import can’t disturb an official who already claimed a slot. Re-using a game’s externalId under a different league is refused with 409 {"error":"external_id_conflict"} and the existing game is left untouched — a game can’t be moved between leagues by re-posting.400 invalid_startsAt:<value>), earlier games in the array are already persisted. Recovery is simple if every game has an externalId: fix the bad input and re-post the entire batch — the upsert makes the retry safe. Always send externalIds on imports.
MCP: whistle_create_league, then whistle_create_games.
List games and assignments
Read-only; safe to run anytime. Neither endpoint is paginated.200 {"data": [ … ]}. Game status is a rollup of its positions: open | claimed | completed | cancelled. Each assignment includes the game summary, the official (id, name, email), and the payment record if one exists.
Prefer webhooks over polling these endpoints —
position.claimed, position.released, and game.completed tell you the moment coverage changes.whistle_list_games, whistle_list_assignments.
Add and approve an official
Officials are referenced by your(source, externalId) — not by email. Email is an optional bridge that links them to their real Whistle account.
201 {"data": {"membershipId": "…", "userId": "…", "name": "Jordan Ref", "status": "approved", …}}. Save userId — assign and remove calls take it.
- Omitting
status(or sending anything except"pending") approves the official immediately. Send"status": "pending"to stage them for organizer approval instead. - Idempotency: re-posting the same
(source, externalId)re-approves/updates the same person (updatesemail/nameonly when provided) — no duplicate memberships. A repeat emitsofficial.approvedinstead ofofficial.joined. 409 user_is_organizermeans that person runs the league — they can’t also be added as an official.
whistle_add_official.
Assign and unassign an official
The official must already be an approved league member (previous recipe), or you’ll get409 not_a_league_official.
200 with the full game detail — the position now shows your official and "status": "claimed". Emits position.claimed.
Retry guidance: claims are race-safe. 409 position_taken means someone claimed the slot first — re-fetch the game and pick another open position (or stop). 409 no_open_position / position_not_open are terminal for that slot; don’t blind-retry a 409.
200 with the game detail; the position is open again. Emits position.released. The positionId query parameter is required.
MCP: whistle_assign_official, whistle_unassign_position (destructive — confirm).
Register and verify webhooks
201 including "secret": "whsec_…" — shown in this response only, never retrievable again. Store it immediately; you need it to verify signatures. An empty/omitted events array subscribes to all events. The endpoint is owned by your key’s source and receives events only for leagues in your scope.
Verify it’s receiving:
status (pending | delivered | failed | exhausted), attempts, lastStatus, and the exact payload sent. Failed deliveries retry automatically — see Webhooks for the schedule, signature verification code, and a secure receiver example.
Retry guidance: registering the same URL twice creates two endpoints (registration is not idempotent — it’s the one write here without an externalId upsert). Before registering, GET /webhooks and check whether your URL is already present.
MCP: whistle_register_webhook, whistle_list_webhook_deliveries, whistle_delete_webhook (destructive — confirm).
