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

# Coach API

> Read your roster's fueling data and configure alerts, reports, and webhooks

# Coach API

The Coach API exposes a coach's Saturday surface over a coach-scoped REST API, and over an [MCP connector](/guides/coach-connector): the per-athlete fueling rollup, AI reports, the roster's needs-attention markers, and the notification and alerting settings.

It is built for a coach who wants to automate fueling monitoring: read who needs attention, pull a report, and configure alert rules and digests across a whole roster programmatically.

<Note>
  **Tier requirements.** Coach endpoints require the Pro Coach tier or above (Pro Coach, Head Coach, Business, Enterprise). Two further gates sit above that line: minting a coach API key in the portal requires Business or Enterprise plus an org-admin role, and webhook *delivery* requires Business or Enterprise. A Pro Coach or Head Coach therefore reaches the whole surface through the OAuth connector, and reaches it through an API key only if someone on a Business plan minted one.
</Note>

<Note>
  **When a tier lapses.** On an OAuth token, the coach tools stop resolving and coach endpoints return `404`, while the coach keeps their own athlete data. There is no error and no separate "downgrade" call. On a coach API key there is no athlete-self facet to fall back to, so the request is refused outright with `403 coach_tier_required`.
</Note>

## Authentication

Two ways to authenticate as a coach. Both resolve to the same coach identity and roster.

| Method                    | Token                              | Best for                                      |
| ------------------------- | ---------------------------------- | --------------------------------------------- |
| **Coach API key**         | `cp_live_…` / `cp_test_…` (Bearer) | Scripts, automations, server-to-server        |
| **OAuth2 (coach scopes)** | OAuth2 access token (Bearer)       | The Claude.ai connector / user-delegated apps |

Coach API keys are minted in the [coach portal](https://coach.saturday.fit) under **[Admin → API Keys](https://coach.saturday.fit/admin/api-keys)**. Pass the key as a Bearer token:

```bash theme={null}
curl -H "Authorization: Bearer cp_live_..." https://api.saturday.fit/v1/coach/roster
```

OAuth2 coaches use the [connector OAuth flow](/guides/oauth2) with coach scopes (`coach:roster`, `coach:reports`, `coach:alerts`, `coach:webhooks`). Because a coach is also an athlete, the same OAuth token acts as an athlete-self token on `/v1/athletes/*`.

## Scopes and capabilities

The coach's token or key carries scopes that map to capabilities. A route is reachable only when the token confers the matching capability; otherwise it returns the uniform `404`.

| Capability                       | OAuth scope      | API-key scope                                          | Grants                                                     |
| -------------------------------- | ---------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| Read roster + rollups + sessions | `coach:roster`   | `roster:read`, `roster:write`, `org:read`, `org:write` | `GET /v1/coach/roster`, `…/fueling-rollup`, `…/sessions/…` |
| Read reports + digest            | `coach:reports`  | `org:read`, `org:write`                                | `…/report`, `/v1/coach/roster/digest`                      |
| Write alert + report config      | `coach:alerts`   | `roster:write`, `org:write`                            | `/v1/coach/config/*`                                       |
| Manage webhooks                  | `coach:webhooks` | `webhooks:manage`                                      | `/v1/coach/webhooks/*`                                     |

The two vocabularies are unioned, never translated, so a key keeps exactly the reach its scopes confer. `roster:read` alone does not open the report routes; pick `org:read` or higher when a key needs reports. Keys minted before scoped keys existed carry a single `*` scope and hold all four capabilities.

<Warning>
  **Roster confinement.** Every `{athlete_uid}` you pass is checked against your roster. An athlete who isn't on your roster, and an athlete who doesn't exist, both return `404 resource_not_found`. The two cases are indistinguishable, so the API never reveals whether an athlete exists.
</Warning>

## Reads

### List the roster

`GET /v1/coach/roster` returns every athlete you coach, each with a needs-attention summary over the look-back window.

```bash theme={null}
curl -H "Authorization: Bearer cp_live_..." \
  "https://api.saturday.fit/v1/coach/roster?window=14"
```

| Query param | Values          | Default                       |
| ----------- | --------------- | ----------------------------- |
| `window`    | `7`, `14`, `30` | your configured report window |

```json theme={null}
{
  "coach_uid": "coach_abc",
  "window": 14,
  "athletes": [
    {
      "athlete_uid": "ath_123",
      "flagged": true,
      "flagged_count": 2,
      "top_reasons": ["Sodium 57%", "1 symptom"],
      "session_count": 6
    }
  ]
}
```

The markers come from Saturday's single shared concern definition, so they match the coach portal table and the digest exactly. An athlete who fueled well shows `flagged: false`.

### Roster digest (flagged-only)

`GET /v1/coach/roster/digest` returns the same data for athletes who crossed a concern bar this window, most-flagged first. Athletes who did not cross a bar are omitted, which is what makes this the useful call on a large roster.

```json theme={null}
{
  "coach_uid": "coach_abc",
  "window": 7,
  "flagged_count": 3,
  "total_count": 48,
  "flagged": [
    {
      "athlete_uid": "ath_123",
      "flagged": true,
      "flagged_count": 2,
      "top_reasons": ["Sodium 57%", "1 symptom"],
      "session_count": 6
    }
  ]
}
```

`flagged` carries the same entry shape as `GET /v1/coach/roster` above, ordered most-flagged first. `flagged_count` counts the entries in it; `total_count` counts the whole roster.

### Per-athlete fueling rollup

`GET /v1/coach/athletes/{athlete_uid}/fueling-rollup` returns one athlete's in-window sessions (the same table the portal renders) plus the concern summary and the resolved cutoffs that produced the markers.

| Query param | Values                    | Default                |
| ----------- | ------------------------- | ---------------------- |
| `window`    | `7`, `14`, `30`           | your configured window |
| `focus`     | `worst`, `rolling`, `key` | your configured focus  |

```json theme={null}
{
  "athlete_uid": "ath_123",
  "window": 14,
  "focus": "rolling",
  "sessions": [
    {
      "activity_id": "act_789",
      "date_millis": 1749480000000,
      "type": "ride",
      "duration_min": 210,
      "is_race": false,
      "carb_pct": 0.82,
      "sodium_pct": 0.57,
      "fluid_pct": 0.94,
      "consumed_carb_g": 172,
      "consumed_sodium_mg": 1140,
      "consumed_fluid_ml": 2350,
      "user_rating": 3,
      "symptoms": { "cramp": 1 },
      "sleep_hours": 6.5
    }
  ],
  "concern": {
    "flagged": true,
    "flagged_count": 2,
    "top_reasons": ["Sodium 57%", "1 symptom"]
  },
  "settings_resolved": {
    "report_window_days": 14,
    "report_focus": "rolling",
    "concern_carb_cutoff": 0.7,
    "concern_sodium_cutoff": 0.7,
    "concern_fluid_cutoff": 0.7,
    "hyponatremia_fluid_min": 0.9,
    "hyponatremia_sodium_max": 0.5
  }
}
```

The session object above is abbreviated. Each entry also carries the as-used and suggested prescription totals, prep fidelity, leftover reuse, report source and completeness, intensity, the profile snapshot, opaque `vessel_reports` and `weather` maps, and the derived `adherence_vs_suggested`, `dial_down_gap`, and `per_hour` triples.

The adherence fractions are uncapped, so a value above `1.0` is genuine over-consumption rather than an error. Every missing value stays `null`, never `0`, and an absent `symptoms` key is not a zero.

### AI report

`GET /v1/coach/athletes/{athlete_uid}/report` returns the AI-generated fueling report: a third-person narrative grounded only in the athlete's own numbers, plus the structured concern summary behind it, so an agent can quote the prose or compute on the data.

| Query param | Values                    | Notes                                                   |
| ----------- | ------------------------- | ------------------------------------------------------- |
| `window`    | `7`, `14`, `30`           | look-back                                               |
| `focus`     | `worst`, `rolling`, `key` | report focus                                            |
| `refresh`   | `true`                    | force regeneration even if a cached report exists       |
| `format`    | `pdf`                     | return the report as a downloadable PDF instead of JSON |

```json theme={null}
{
  "athlete_uid": "ath_123",
  "window": 14,
  "focus": "rolling",
  "narrative": "Over the last two weeks, this athlete …",
  "concern": {
    "flagged": true,
    "flagged_count": 2,
    "top_reasons": ["Sodium 57%", "1 symptom"]
  },
  "generated_at": 1749500000000,
  "latest_session_ms": 1749480000000,
  "from_cache": true
}
```

Served from cache unless a newer session has landed or `refresh=true`.

### Session detail

`GET /v1/coach/athletes/{athlete_uid}/sessions/{activity_id}` drills into one session: the full per-session projection (planned-vs-actual fueling, symptoms, vessel reports, weather, sleep) plus the concern markers that session crossed.

## Configuration

Most of the Coach API is a configuration surface. Anything a coach can configure in the portal is configurable here: channels, triggers, per-nutrient thresholds, the overall/group/athlete scope hierarchy, consolidation, cadence, quiet hours, and AI-report defaults. The portal UI and the API are two views of one config model.

<Info>
  **Scope precedence.** Config applies at one of three scopes: `overall` (the whole roster), `group` (a coach group), or `athlete` (one athlete). When resolving what an athlete sees, the most specific scope wins (athlete over group over overall). `scope_id` is required for `group` and `athlete`, and omitted for `overall`.
</Info>

### Notification rules

`GET /v1/coach/config/notification-rules?scope=overall` reads the rules set at exactly that scope, not the merged resolution.

`PUT /v1/coach/config/notification-rules` replaces the rule set at a scope. It is an idempotent upsert: re-running with the same body is a no-op, so no per-request idempotency key is needed.

```bash theme={null}
curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \
  -H "Authorization: Bearer cp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "group",
    "scope_id": "grp_elite",
    "rules": {
      "notification_rules": {
        "under_fuel": { "enabled": true, "channels": ["email", "webhook"], "cadence": "realtime", "urgent_threshold": 0.6 },
        "hyponatremia_pattern": { "enabled": true, "channels": ["push"], "cadence": "realtime" }
      },
      "combinators": [
        { "trigger_a": "under_fuel", "trigger_b": "symptom", "channel": "push", "cadence": "realtime" }
      ],
      "quiet_hours": { "enabled": true, "start": "22:00", "end": "06:00", "tz": "America/Denver" }
    }
  }'
```

**Triggers:** `under_fuel`, `symptom`, `low_rating`, `hyponatremia_pattern`, `dial_down`, `sleep_trend`, `went_quiet`.

**Channels:** `in_portal`, `email`, `push`, `webhook`. (SMS is not yet supported.)

**Cadence:** `realtime` (sent as it happens) or `digest` (bundled into one daily item per athlete).

**Thresholds** are fractions in `(0,1]`. `urgent_threshold` is the urgent band. `amber_threshold` is optional and decouples this trigger's marker line from the shared cutoff; omit it to use the resolved concern cutoff.

**Combinators** are bounded two-trigger ANDs: both legs must be markers on the same session ("sodium short AND a cramp"). Exactly two triggers, with no OR, NOT, or nesting.

### Presets

`POST /v1/coach/config/preset` applies a named starting point at a scope, which you can then tweak rule by rule.

```bash theme={null}
curl -X POST https://api.saturday.fit/v1/coach/config/preset \
  -H "Authorization: Bearer cp_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "scope": "overall", "preset": "balanced" }'
```

| Preset      | Behavior                             |
| ----------- | ------------------------------------ |
| `hands_off` | in-portal only, no pings             |
| `balanced`  | core concerns on email, daily digest |
| `hands_on`  | all triggers, real-time urgent push  |

### AI-report & concern settings

`GET` / `PUT /v1/coach/config/report-settings` reads/upserts the report window/focus defaults and any overridden concern cutoffs at a scope. Unset fields fall through to the broader scope.

```bash theme={null}
curl -X PUT https://api.saturday.fit/v1/coach/config/report-settings \
  -H "Authorization: Bearer cp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "athlete",
    "scope_id": "ath_123",
    "settings": {
      "ai_report_window_days": 30,
      "ai_report_focus": "key",
      "concern_sodium_cutoff": 0.65
    }
  }'
```

<Warning>
  **Athlete data is read-only.** The Coach API never writes an athlete's fueling data, debriefs, or prescriptions. A coach can configure only their own alerts, reports, groups, and thresholds.
</Warning>

## Webhooks

A webhook is another delivery channel on the alerts you already configure (`"channels": ["webhook"]`). Register an endpoint, then select `webhook` as a channel on any rule.

<Note>
  Webhook delivery requires the Business or Enterprise tier. Registration succeeds at Pro Coach and Head Coach, but no events are delivered until the tier qualifies, and delivery stops within the same billing check if the tier later drops.
</Note>

`POST /v1/coach/webhooks` registers an endpoint and returns the signing secret once.

```bash theme={null}
curl -X POST https://api.saturday.fit/v1/coach/webhooks \
  -H "Authorization: Bearer cp_live_..." \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://my-system.example.com/saturday", "events": ["concern.detected", "athlete.needs_attention"] }'
```

```json theme={null}
{
  "id": "wh_abc",
  "coach_uid": "coach_abc",
  "url": "https://my-system.example.com/saturday",
  "events": ["concern.detected", "athlete.needs_attention"],
  "active": true,
  "created_at": 1749500000000,
  "secret": "whsec_…"
}
```

<Warning>
  `secret` appears in this response and never again. `GET /v1/coach/webhooks` omits it, and there is no endpoint that re-reads it. Store it when you register, or delete the endpoint and register a new one to get a fresh secret.
</Warning>

| Method · path                          | Action                                     |
| -------------------------------------- | ------------------------------------------ |
| `GET /v1/coach/webhooks`               | list endpoints (secrets never returned)    |
| `POST /v1/coach/webhooks`              | register (secret returned once)            |
| `DELETE /v1/coach/webhooks/{id}`       | delete an endpoint                         |
| `POST /v1/coach/webhooks/{id}/disable` | disable (stop delivery, keep the endpoint) |
| `POST /v1/coach/webhooks/{id}/enable`  | re-enable                                  |

**Event types:** `concern.detected` and `athlete.needs_attention`, which an empty `events` list subscribes you to, plus `coach.message.sent`, which is opt-in and only fires for Enterprise coaches. The URL must be public `https`; internal, metadata, and loopback URLs are rejected at registration. Deliveries are signed with HMAC-SHA256 in the `X-Saturday-Signature` header using the secret returned at registration, retried with backoff, and auto-disabled after 15 consecutive failures. See [Webhooks](/guides/webhooks) for verifying signatures.

## Full identity

Coach API responses carry full athlete identity: names and data exactly as the coach sees them in the portal. The coach owns the coaching relationship, so the API has no de-identification layer (the export feature's identity levels are a portal and export concern). Handling that PII downstream is your responsibility. See the [data policy](/guides/data-policy).

## Rate limits

Coach principals get 50 requests per second sustained with a burst of 500, so a 500-athlete roster pull or digest completes in one turn without paging penalties. These limits are set for abuse protection rather than capacity management. See [rate limiting](/rate-limiting).

## SDKs

Both the [TypeScript](https://github.com/SaturdayInc/saturday-node) and [Python](https://github.com/SaturdayInc/saturday-python) SDKs expose the coach surface as a `coach` resource:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import Saturday from '@saturdayinc/sdk';

  const saturday = new Saturday({ apiKey: 'cp_live_...' });

  const digest = await saturday.coach.rosterDigest({ window: 7 });
  const report = await saturday.coach.report('ath_123', { window: 14 });

  await saturday.coach.applyPreset({ scope: 'overall', preset: 'balanced' });
  const wh = await saturday.coach.registerWebhook('https://my-system.example.com/saturday');
  // wh.secret is returned once; store it now
  ```

  ```python Python theme={null}
  from saturday import Saturday

  client = Saturday(api_key="cp_live_...")

  digest = client.coach.roster_digest(window=7)
  report = client.coach.report("ath_123", window=14)

  client.coach.apply_preset(scope="overall", preset="balanced")
  wh = client.coach.register_webhook("https://my-system.example.com/saturday")
  # wh["secret"] is returned once; store it now
  ```
</CodeGroup>
