Skip to main content

The model at a glance

League ─┬─ Game ─── GamePosition ─── Claim (one per position) ─── Payment (one per position)
        └─ Membership (role: organizer | official) ─── User
  • 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 Whistle id (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 to 0.
  • 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): openclaimedcompleted, with cancelled reachable from open/claimed. A game’s status is a rollup of its positions.
StatusPosition meansGame means
openSlot needs an officialAt least one slot unfilled
claimedAn official holds the slotAll slots filled
completedWorked; payment createdOrganizer marked the game complete
cancelledSlot cancelledGame cancelled
Notes:
  • Completing a game is an in-app organizer action — there is no API endpoint for it. You observe it via the game.completed webhook.
  • Cancelling a game via DELETE /games/{gameId} cancels its open positions; claimed and completed positions are preserved.
  • Read official defensively: a position’s official is independent of its status. A cancelled position can still carry a populated official — that’s a no-show record (the organizer cancelled the slot unpaid but kept the claim for the books, emitting official.no_show).

Claim lifecycle

A claim’s claimStatus 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: pendingapproved, 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.
statusMeaning
pendingCreated, not yet charged. Offline-mode payments stay here until the organizer marks them paid.
processingPlatform charge created, awaiting settlement.
paidSettled (paidAt set). Emits payment.paid.
requires_cardPlatform charge needs card authentication — recoverable.
failedPlatform 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).
Last modified on July 10, 2026