> ## Documentation Index
> Fetch the complete documentation index at: https://huddleup-sports.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Resource Model & Data Formats

> Whistle's resource model — leagues, games, positions, claims, officials, payments — their lifecycles, field definitions, identifiers, money and time formats.

## 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](/developers/authentication#what-your-key-can-access).

## 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](/developers/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                     |

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`: `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](/developers#integration-status).

## Endpoint and tool reference

* REST: the [API reference](/developers/api-reference) documents every endpoint, generated from the [OpenAPI spec](https://huddleup-sports.com/docs/api-reference/openapi.json).
* MCP: the [20-tool list](/developers/quickstart-mcp#the-20-tools) maps one-to-one onto the 20 resource operations (every endpoint except the index).
