diff --git a/authentication.mdx b/authentication.mdx index b6e51f2..ed184f3 100644 --- a/authentication.mdx +++ b/authentication.mdx @@ -83,6 +83,8 @@ curl -X POST https://api.saturday.fit/v1/partner/api-keys \ `scopes` accepts `["*"]` for unrestricted, `["read"]` for `GET`/`HEAD`/`OPTIONS`, and `["write"]` for `POST`/`PUT`/`PATCH`/`DELETE`. A read-write key needs both listed: `write` does not imply `read`. A request outside a key's scopes gets a `403` with code `insufficient_scope`. +`environment` is optional and defaults to the environment of the host you call: `live` on `api.saturday.fit`, `test` on the sandbox host. Naming the other environment, or any other value, is a `400` with code `invalid_value`, because a key record only ever authenticates on the host that issued it. + Save the key immediately. The full value is returned once at creation. The `id` in that response is what the rotate and revoke endpoints take, so store it with the key. @@ -154,4 +156,4 @@ Authentication and authorization failures share the standard [error envelope](/e | `coach_tier_required` | `authorization_error` | 403 | A `cp_` key whose coach is below Pro Coach | | `internal_error` | `api_error` | 503 | Key verification is temporarily unavailable. Retry rather than rotating a working key | -A key used against the wrong environment resolves as `invalid_api_key`, because the key does not exist in that environment's records. +A key used against the wrong environment fails closed as `invalid_api_key`, and the message names the environment the key was issued for. Each environment keeps separate key records, and a record whose environment is not the host's is refused even if it is present. diff --git a/guides/athletes.mdx b/guides/athletes.mdx index 9a5dc81..1c13507 100644 --- a/guides/athletes.mdx +++ b/guides/athletes.mdx @@ -179,7 +179,7 @@ The settings object is written as a unit. Send the athlete's complete settings o The same replacement rule applies if you send a `settings` object through `PATCH /v1/athletes/{id}`. -On update and on calculate, Saturday rejects `carb_upper_limit_override` outside 50-150 g/hr and `weight_kg` outside 20-250 with a 400 rather than clamping, so a bad value in your data surfaces at the boundary instead of distorting a prescription. +On create, on update and on calculate, Saturday rejects `carb_upper_limit_override` outside 50-150 g/hr and `weight_kg` outside 20-250 with a `400` of code `out_of_range` rather than clamping, so a bad value in your data surfaces at the boundary instead of distorting a prescription. Batch create reports the same code per item. ## Deleting an athlete diff --git a/guides/coach-api.mdx b/guides/coach-api.mdx index eb8c640..faf8647 100644 --- a/guides/coach-api.mdx +++ b/guides/coach-api.mdx @@ -391,7 +391,9 @@ Coach API responses identify each athlete by Saturday UID, the same identifier t ## Rate limits -Coach requests, on either credential, are metered in the same partner bucket as the Claude connector. An OAuth coach token also passes through the per-athlete bucket that applies to every athlete-delegated caller, keyed on the coach's UID. A roster pull or digest is one request whatever the roster size. Every response carries the rate-limit headers and a `429` carries `Retry-After`; see [rate limiting](/rate-limiting). +Coach requests to `/v1/coach/*`, on either credential, are metered in a bucket of the coach's own: 50 requests per second sustained with a burst of 500, sized so a roster-wide fan-out never trips it. That bucket replaces the Claude connector's shared partner bucket and the per-athlete buckets on the coach surface. Off the coach surface, an OAuth coach token is metered as the athlete it also is, in the per-athlete buckets that apply to every athlete-delegated caller. A roster pull or digest is one request whatever the roster size. Every response carries the rate-limit headers and a `429` carries `Retry-After`; see [rate limiting](/rate-limiting). + +A coach API key reaches `/v1/coach/*` only; on any other endpoint it is refused with `403 insufficient_scope`. ## SDKs diff --git a/guides/webhooks.mdx b/guides/webhooks.mdx index d029680..0fc77a1 100644 --- a/guides/webhooks.mdx +++ b/guides/webhooks.mdx @@ -138,6 +138,7 @@ Every delivery has this envelope. `id` is a UUID and `created_at` is a Unix time "type": "prescription.calculated", "created_at": 1765467600000, "data": { + "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "tier": "full", "carb_g_per_hr": 60, "sodium_mg_per_hr": 500, @@ -165,11 +166,11 @@ What `data` carries depends on the event: | `athlete.deleted` | `athlete_id` only | | `activity.created`, `activity.updated` | The activity record | | `activity.deleted` | `athlete_id` and `activity_id` only | -| `prescription.calculated` | From `POST /v1/nutrition/calculate`: the calculate response, at whichever tier the caller received, teaser included. From an activity calculation: the stored prescription (`carb_g_per_hr`, `sodium_mg_per_hr`, `fluid_ml_per_hr`, the three totals, `calculated_at`) | +| `prescription.calculated` | From `POST /v1/nutrition/calculate`: the calculate response, at whichever tier the caller received, teaser included, plus `athlete_id` when the request named an athlete. From an activity calculation: the stored prescription (`carb_g_per_hr`, `sodium_mg_per_hr`, `fluid_ml_per_hr`, the three totals, `calculated_at`) plus `athlete_id` and `activity_id` | | `feedback.submitted` | The feedback record: `rating`, `notes`, `created_at` | | `subscription.created`, `subscription.cancelled` | The payloads above | -The prescription payload names no athlete or activity. A calculation you trigger through the API is identifiable by its response; the webhook is the signal that one ran. +A calculation with no athlete (a `POST /v1/nutrition/calculate` without `athlete_id`) carries neither id; `tier` tells you which tier the caller received. Athlete, activity, and event identifiers are all UUIDs; they carry no prefix, so do not pattern-match on one.