The model at a glance
- A league contains games and members.
- A game has one or more positions (the slots to fill — “Umpire”, “Referee”). Partner-imported games get exactly one position.
- An official claims a position (or you assign them via the API). One claim per position.
- When an organizer completes a game, a payment is created per worked position. One payment per position.
Identifiers and idempotency
Every resource has a Whistleid (a cuid string). Leagues, games, and users additionally carry your (source, externalId) pair:
source— your system’s namespace (e.g."huddleup"). It is bound to your API key and stamped on your writes automatically — omit it from request bodies or echo your own value (anything else →400 source_mismatch)."whistle"is reserved for rows created in-app.externalId— your stable id for the record.
(source, externalId) is unique per resource type and drives idempotent upserts on POST /leagues, POST /leagues/{leagueId}/games, and POST /leagues/{leagueId}/officials. Re-using a game’s externalId under a different league is rejected with 409 external_id_conflict rather than moving the game. Records created without an externalId can’t be upserted — always send one from an integration.
Your source is also your visibility boundary: the key reads and mutates only leagues in its namespace plus explicit grants — see Authentication.
Money and time
- Money in: dollars, as
payDollars(number). Rounded to the nearest cent; negative values clamp to0. - Money out: both units — integer cents (
payCents,amountCents) and float dollars (payDollars,amountDollars). Use the cents fields for arithmetic. - Currency: ISO 4217, lowercase (
"usd"), set per league. - Time in: ISO 8601 strings (
"2026-08-15T18:00:00Z"). Send UTC with an explicit offset — parsing is lenient, but don’t rely on that. - Time out: ISO 8601 UTC with milliseconds:
"2026-08-15T18:00:00.000Z".
Pagination and filtering
There is none. Every list endpoint returns the full set (games soonest-first, leagues/payments/webhooks newest-first, officials oldest-first). The only query parameter is?limit= on webhook deliveries (default 50, max 200). Plan for this if you sync large leagues, and prefer webhooks to repeated full-list polling.
Game & position lifecycle
Statuses (shared by games and positions):open → claimed → completed, with cancelled reachable from open/claimed. A game’s status is a rollup of its positions.
| Status | Position means | Game means |
|---|---|---|
open | Slot needs an official | At least one slot unfilled |
claimed | An official holds the slot | All slots filled |
completed | Worked; payment created | Organizer marked the game complete |
cancelled | Slot cancelled | Game cancelled |
- Completing a game is an in-app organizer action — there is no API endpoint for it. You observe it via the
game.completedwebhook. - Cancelling a game via
DELETE /games/{gameId}cancels its open positions; claimed and completed positions are preserved. - Read
officialdefensively: a position’sofficialis independent of itsstatus. Acancelledposition can still carry a populatedofficial— that’s a no-show record (the organizer cancelled the slot unpaid but kept the claim for the books, emittingofficial.no_show).
Claim lifecycle
A claim’sclaimStatus is always "confirmed" today (the only value in v1). Claims are created by an official claiming in-app or by POST /games/{gameId}/assign, and deleted (slot reopened) by withdrawal, unassignment, or the official being removed from the league.
Official (membership) lifecycle
status: pending → approved, or denied. API-added officials default to approved (send "status": "pending" to stage them). List endpoints return all statuses — filter on status yourself; a pending or denied official cannot be assigned (409 not_a_league_official).
Removing an official (DELETE /leagues/{leagueId}/officials/{userId}) reopens every position they had claimed in that league.
Payment lifecycle
Payments are read-only in the API (GET /leagues/{leagueId}/payments); they’re created when an organizer completes a game and settled by in-app action.
status | Meaning |
|---|---|
pending | Created, not yet charged. Offline-mode payments stay here until the organizer marks them paid. |
processing | Platform charge created, awaiting settlement. |
paid | Settled (paidAt set). Emits payment.paid. |
requires_card | Platform charge needs card authentication — recoverable. |
failed | Platform charge declined (failureCode/failureMessage set). Emits payment.failed. |
mode per payment: "offline" (organizer settles outside Whistle and marks paid — the fully supported path today) or "platform" (Stripe). See the integration status note.
Endpoint and tool reference
- REST: the API reference documents every endpoint, generated from the OpenAPI spec.
- MCP: the 20-tool list maps one-to-one onto the 20 resource operations (every endpoint except the index).

