> ## 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.

# Errors & Safe Agent Behavior

> Whistle API error classification — validation, auth, not-found, conflict — retry rules, and required confirmation behavior for AI agents before destructive actions.

## Error format

Every error is `{"error": "<code>"}` — one snake\_case code, no message or details object. Two codes carry a parameter after a colon: `invalid_startsAt:<the bad value>` and `unknown_event:<the bad name>`.

The API uses exactly four error statuses: `400`, `401`, `404`, `409`. It never returns `403`. Unexpected server faults surface as plain `500`s.

## Classification and retry rules

| Class          | Status                  | Codes                                                                                                                                                                                                                                                            | Retry?                                                                                                                                                                                                                                                                                                                                                         |
| -------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Auth**       | 401                     | `unauthorized`                                                                                                                                                                                                                                                   | **No.** The key is missing, wrong, or revoked. Fix the credential; see [Authentication](/developers/authentication).                                                                                                                                                                                                                                           |
| **Validation** | 400                     | `invalid_json`, `name_required`, `source_reserved`, `source_mismatch`, `no_games`, `too_many_games`, `invalid_startsAt(:<value>)`, `invalid_status`, `externalId_required`, `positionId_required`, `https_url_required`, `unknown_event:<name>`, `create_failed` | **Not as-is.** Fix the input, then re-send. `source_mismatch` means the body's `source` differs from your key's — omit the field. For batch game imports, remember earlier items may already be persisted — re-post the whole corrected batch with `externalId`s.                                                                                              |
| **Not found**  | 404                     | `league_not_found`, `game_not_found`, `official_not_found`, `position_not_found`, `not_a_member`, `endpoint_not_found`                                                                                                                                           | **No.** The id is wrong, the resource is gone, **or it's outside your key's scope — deliberately indistinguishable.** Re-list to re-discover ids; don't conclude a resource doesn't exist platform-wide.                                                                                                                                                       |
| **Conflict**   | 409                     | `user_is_organizer`, `not_an_official`, `not_a_league_official`, `no_open_position`, `position_not_open`, `position_taken`, `position_not_claimed`, `external_id_conflict`                                                                                       | **Not blindly.** State disagrees with your intent. Re-fetch, then decide. `position_taken` means you lost a concurrent claim race — picking a *different* open position is a safe follow-up. `external_id_conflict` means that game `externalId` already belongs to a different league — a game can't be moved by re-posting; fix the id or the target league. |
| **Transient**  | 5xx / network / timeout | —                                                                                                                                                                                                                                                                | **Yes, with care.** Retry with backoff **only** requests that are safe to repeat: all GETs, and the three upsert writes (create league, create games, add official) with stable `externalId`s. Everything else — see below.                                                                                                                                    |

## Idempotency is your retry safety net

Exactly **three** writes are idempotent upserts on `(source, externalId)` and safe to repeat: `POST /leagues`, `POST /leagues/{leagueId}/games`, `POST /leagues/{leagueId}/officials`. **Always send a stable `externalId` from an integration** — it is what makes a timeout recoverable by simple re-send.

Every other write is **not** idempotent — never auto-retry after an ambiguous failure (timeout, dropped connection) without checking state first:

* `POST /games/{gameId}/assign` — its `externalId`/`source` fields *identify the official*; they do **not** make the assignment idempotent. Re-fetch the game to see whether the claim landed before re-sending.
* `POST /webhooks` — registering twice creates two endpoints. `GET /webhooks` first.
* Any `DELETE`.

## Rules for AI agents

If you are an agent operating this API (via REST or the [MCP tools](/developers/quickstart-mcp)), follow these rules:

<Steps>
  <Step title="Reads are free; writes need intent">
    List/get calls are always safe. Only call write tools when the user's request actually requires the change.
  </Step>

  <Step title="Summarize and confirm before destructive actions">
    Before any of the following, present a plain-language summary of exactly what will change (which league/game/official, by name and id) and get explicit confirmation:

    * **Cancel a game** (`DELETE /games/{gameId}` / `whistle_cancel_game`) — cancels open slots.
    * **Remove an official** (`DELETE /leagues/{leagueId}/officials/{userId}` / `whistle_remove_official`) — also reopens every position they'd claimed.
    * **Unassign a position** (`DELETE /games/{gameId}/assign` / `whistle_unassign_position`) — deletes the claim.
    * **Delete a webhook** (`DELETE /webhooks/{webhookId}` / `whistle_delete_webhook`) — removes delivery history; endpoint may belong to another integration.
    * **Bulk mutations** — any loop that writes to many records (mass game updates, batch official changes). Confirm the batch as a whole, with a count.
  </Step>

  <Step title="Never auto-repeat writes without stable identifiers">
    Repeating a write is safe only when it's an upsert keyed on `(source, externalId)`. If you don't have a stable `externalId` for a record, a repeated POST creates a duplicate — stop and verify state instead of retrying.
  </Step>

  <Step title="Treat 409s as information, not failure">
    A conflict means the world changed (someone claimed the slot, the official was already removed). Re-fetch, report the actual state, and re-plan rather than forcing the original action.
  </Step>

  <Step title="Respect the scope boundary">
    Your key sees only leagues and webhook endpoints in your `source` namespace (plus explicit grants) — out-of-scope resources answer `404` exactly like nonexistent ones. Don't interpret a 404 as "gone" and re-create records on that basis alone; re-list what your key *can* see and reconcile from there.
  </Step>
</Steps>

<Warning>
  Never place a real API key in a tool config file, chat message, or code sample. Connect via environment variables as shown in the [MCP quickstart](/developers/quickstart-mcp).
</Warning>
