Skip to main content
Safe, self-contained flows for the most common integration tasks. All examples use $WHISTLE_API_KEY and the production base URL:
Throughout, 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.
1

Upsert the league

Expected: 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.
2

Batch-import games (1–500 per request)

A batch import is a bulk mutation: with externalIds it upserts, so re-posting can rewrite sport, startsAt, and location on up to 500 existing games in one call — and it is not transactional. Agents and automations should summarize the target league, source, and game count and get confirmation before sending.
Expected: 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.
Retry guidance: the batch is inserted sequentially, not in a transaction. If one game fails validation (e.g. 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.
Expected: 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.
MCP: 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.
Expected: 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 (updates email/name only when provided) — no duplicate memberships. A repeat emits official.approved instead of official.joined.
  • 409 user_is_organizer means that person runs the league — they can’t also be added as an official.
MCP: whistle_add_official.

Assign and unassign an official

The official must already be an approved league member (previous recipe), or you’ll get 409 not_a_league_official.
Expected: 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.
Expected: 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

Expected: 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:
Each delivery shows 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).
Last modified on July 10, 2026