diff --git a/access.mdx b/access.mdx index 5d5cd6d..d355fac 100644 --- a/access.mdx +++ b/access.mdx @@ -1,13 +1,13 @@ --- title: 'Getting Access' -description: 'Which lane fits you — self-serve API, coach portal, or partnership' +description: 'Which lane fits you: self-serve API, coach portal, or partnership' sidebarTitle: 'Getting Access' icon: 'door-open' --- # Getting Access -There are four ways to build with Saturday. One question routes you: **where do your athletes live?** +There are four ways to build with Saturday. One question routes you: where do your athletes live? | Lane | Who | Athletes live in… | Auth | Money | |------|-----|-------------------|------|-------| @@ -20,23 +20,23 @@ There are four ways to build with Saturday. One question routes you: **where do You're building your own product, tooling, or coaching stack, and athletes live in **your** database. -- **Sign up:** [saturday.fit/api](https://saturday.fit/api) — email → Stripe Checkout → key revealed once + emailed. Programmatic path: `POST /v1/signup` (no auth) returns a hosted checkout URL — built for AI agents acquiring access for their humans. +- **Sign up:** [saturday.fit/api](https://saturday.fit/api). Enter an email, complete Stripe Checkout, and your key is revealed once on the success page and emailed to you. Programmatic path: `POST /v1/signup` (no auth) returns a hosted checkout URL, built for AI agents acquiring access for their humans. - **Price:** $5.39/month flat. Cancel anytime. - **Trial economics:** every athlete you create gets the standard [30-day full-precision trial](/guides/freemium-model#30-day-full-precision-trial) (15 calls day one, then 5/day) automatically. -- **Limits:** 2 requests/second, 200 calls/day per key. Outgrowing them is exactly when we want to hear from you: [support@saturday.fit](mailto:support@saturday.fit). +- **Limits:** 2 requests/second and 200 calls/day, counted per account rather than per key. Outgrowing them is when we want to hear from you: [support@saturday.fit](mailto:support@saturday.fit). ## Coach portal (your athletes use the Saturday app) -If you coach real Saturday-app athletes, the [coach portal](https://coach.saturday.fit) is your lane — roster, alerts, coverage — and your `cp_` API key already lets you build scripts against your roster. See the [Coach API guide](/guides/coach-api). +If you coach real Saturday-app athletes, the [coach portal](https://coach.saturday.fit) is your lane: roster, alerts, coverage. Your `cp_` API key lets you build scripts against your roster. See the [Coach API guide](/guides/coach-api). ## Platform partnership -You run a training platform and want Saturday embedded for your entire user base — partner-namespaced athletes, revenue share, [bundle and team offers](/guides/freemium-model#bundle-offers), webhooks at platform scale. That's a conversation, not a form: [alex@saturdaymorning.fit](mailto:alex@saturdaymorning.fit). +You run a training platform and want Saturday embedded for your entire user base: partner-namespaced athletes, revenue share, [bundle and team offers](/guides/freemium-model#how-discounts-stack), webhooks at platform scale. That one starts as a conversation: [alex@saturdaymorning.fit](mailto:alex@saturdaymorning.fit). - **A big team wanting both** — app athletes *and* custom tooling beyond roster scripts — is partner-shaped. Start the conversation rather than stretching a self-serve key. + A big team wanting both app athletes *and* custom tooling beyond roster scripts is partner-shaped. Start the conversation rather than stretching a self-serve key. ## Discovery for agents -`GET https://api.saturday.fit/v1/` (no auth) self-describes the API: live endpoints, plan, price, trial shape, and both signup paths. Site context: [saturday.fit/llms.txt](https://saturday.fit/llms.txt). +`GET https://api.saturday.fit/v1/` (no auth) self-describes the API: the endpoints currently exposed, plan, price, trial shape, and both signup paths. Site context: [saturday.fit/llms.txt](https://saturday.fit/llms.txt). diff --git a/authentication.mdx b/authentication.mdx index 77748f6..da530e5 100644 --- a/authentication.mdx +++ b/authentication.mdx @@ -6,67 +6,65 @@ sidebarTitle: 'Authentication' # Authentication -Saturday supports three authentication methods depending on your use case: +Saturday has three authentication methods, one per principal type: | Method | Use case | How it works | |--------|----------|--------------| -| **Partner API Key** | Server-to-server partner integration | `sk_*` Bearer token in Authorization header | -| **Coach API Key** | A coach's own scripts/automation against their roster | `cp_*` Bearer token in Authorization header | -| **OAuth2** | Athlete- or coach-delegated access (incl. the Claude connector) | Authorization code flow with PKCE | +| **Partner API Key** | Server-to-server partner integration | `sk_*` Bearer token in the `Authorization` header | +| **Coach API Key** | A coach's own scripts against their roster | `cp_*` Bearer token in the `Authorization` header | +| **OAuth2** | Athlete- or coach-delegated access, including the Claude connector | Authorization code flow with PKCE | -Most partner integrations start with API keys. Add OAuth2 when you need athletes (or coaches) to connect their existing Saturday accounts. Coaches automating against their own roster use a coach API key — see the [Coach API](/guides/coach-api). +Most partner integrations start with API keys. Add OAuth2 when you need athletes or coaches to connect their existing Saturday accounts. Coaches automating against their own roster use a coach API key, covered in the [Coach API](/guides/coach-api). ## API keys -All server-to-server requests use API keys passed in the `Authorization` header as Bearer tokens. +All server-to-server requests pass the key in the `Authorization` header as a Bearer token. ### Key types -Saturday uses prefixed API keys to prevent environment mistakes: +The prefix encodes both the principal and the environment, so a misrouted key fails closed instead of touching the wrong data. | Prefix | Environment | Purpose | |--------|-------------|---------| -| `sk_test_` | Sandbox | Partner key — development and testing. Returns realistic data, no rate limit pressure. | -| `sk_live_` | Production | Partner key — real athlete data. Standard rate limits apply. | -| `cp_test_` | Sandbox | Coach key — a coach's roster surface (`/v1/coach/*`) in test. | -| `cp_live_` | Production | Coach key — a coach's roster surface in production. Requires the Business tier or higher. | +| `sk_test_` | Sandbox | Partner key for development and testing | +| `sk_live_` | Production | Partner key against real athlete data | +| `cp_test_` | Sandbox | Coach key for a coach's roster surface (`/v1/coach/*`) | +| `cp_live_` | Production | Coach key in production. Requires the Business tier or higher | -Coach keys (`cp_*`) are minted in the [coach portal](https://coach.saturday.fit) under **[Settings → API Keys](https://coach.saturday.fit/admin/api-keys)**, not via `api@saturday.fit`. They authenticate the coach to the [Coach API](/guides/coach-api) and are confined to that coach's roster + own config. +Coach keys (`cp_*`) are minted in the [coach portal](https://coach.saturday.fit) under **[API Keys](https://coach.saturday.fit/admin/api-keys)**, not via `api@saturday.fit`. They authenticate the coach to the [Coach API](/guides/coach-api) and are confined to that coach's roster and own config. A coach whose subscription lapses below Pro Coach loses `cp_` key access on the next request. -```bash -# Sandbox -curl -H "Authorization: Bearer sk_test_abc123..." https://api.saturday.fit/v1/nutrition/calculate +Coach keys use their own scope vocabulary, chosen at mint time in the portal: `roster:read`, `roster:write`, `billing:read`, `billing:write`, `org:read`, `org:write`, `webhooks:manage`. That is a different model from the partner-key scopes below, and the two do not mix. -# Production -curl -H "Authorization: Bearer sk_live_xyz789..." https://api.saturday.fit/v1/nutrition/calculate +```bash +curl -H "Authorization: Bearer $SATURDAY_API_KEY" \ + https://api.saturday.fit/v1/nutrition/calculate ``` - **Never expose live keys in client-side code.** API keys should only be used in server-to-server requests. If you suspect a key has been compromised, rotate it immediately. + Never expose live keys in client-side code. API keys belong in server-to-server requests only. If you suspect a key has been compromised, revoke it immediately. ### Getting your keys -1. Contact [api@saturday.fit](mailto:api@saturday.fit) with your platform name and use case -2. You'll receive a sandbox key after vetting -3. Complete integration testing to receive your production key +Self-serve accounts are issued a key at checkout. It is revealed once on the success page and emailed. If it never arrived, `POST /v1/signup/resend` with your checkout session id rotates and re-sends it. -### Using your key +Platform partners are issued keys as part of the agreement: contact [api@saturday.fit](mailto:api@saturday.fit) with your platform name and use case. See [Getting Access](/access) for which lane applies to you. -Include the key in the `Authorization` header on every request: +### Using your key ```python Python +import os import requests -headers = {"Authorization": "Bearer sk_test_abc123def456"} +headers = {"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"} response = requests.get("https://api.saturday.fit/v1/athletes", headers=headers) ``` ```typescript TypeScript const response = await fetch("https://api.saturday.fit/v1/athletes", { - headers: { Authorization: "Bearer sk_test_abc123def456" }, + headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}` }, }); ``` @@ -74,82 +72,86 @@ const response = await fetch("https://api.saturday.fit/v1/athletes", { ### Key management -**Creating additional keys:** +**Creating additional keys.** One key per service or deployment stage keeps a compromise contained and makes rotation a non-event. ```bash curl -X POST https://api.saturday.fit/v1/partner/api-keys \ - -H "Authorization: Bearer sk_live_xyz789..." \ + -H "Authorization: Bearer $SATURDAY_API_KEY" \ -H "Content-Type: application/json" \ - -d '{ "name": "production-nutrition-service", "environment": "live" }' + -d '{ "name": "production-nutrition-service", "environment": "live", "scopes": ["*"] }' ``` +`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`. + - **Save the key immediately.** The full key value is only returned once at creation time. Store it securely — you won't be able to retrieve it later. + 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, and it is not derivable from the key itself, so store it too. -**Rotating keys:** - -Key rotation creates a new key and keeps the old one valid for 72 hours: +**Rotating keys.** Rotation mints a replacement and revokes the old key in the same call, with no grace period. The old key stops working immediately. ```bash -curl -X POST https://api.saturday.fit/v1/partner/api-keys/key_abc123/rotate \ - -H "Authorization: Bearer sk_live_xyz789..." +curl -X POST https://api.saturday.fit/v1/partner/api-keys/{key_id}/rotate \ + -H "Authorization: Bearer $SATURDAY_API_KEY" ``` -**Revoking keys:** +For a cutover with no downtime, do not rotate. Create a second key, deploy it everywhere, confirm traffic has moved, then revoke the first. -Immediately invalidate a key (no grace period): +**Revoking keys.** Revocation takes effect immediately. ```bash -curl -X DELETE https://api.saturday.fit/v1/partner/api-keys/key_abc123 \ - -H "Authorization: Bearer sk_live_xyz789..." +curl -X DELETE https://api.saturday.fit/v1/partner/api-keys/{key_id} \ + -H "Authorization: Bearer $SATURDAY_API_KEY" ``` ## Environments | Environment | Base URL | Data | |-------------|----------|------| -| Sandbox | `https://api.saturday.fit` | Synthetic test data | | Production | `https://api.saturday.fit` | Real athlete data | +| Sandbox | Issued with your `sk_test_` key | Synthetic test data | -Both environments use the same base URL — the API key prefix determines which environment you're operating in. +The key prefix and the base URL travel together. A `sk_test_` key does not authenticate against `api.saturday.fit`. ### Sandbox behavior -- All endpoints work identically to production -- Athlete data is isolated — sandbox athletes are not real people -- Rate limits are relaxed (higher limits for testing) -- Nutrition calculations return realistic but synthetic results +- Athlete data is isolated. Sandbox athletes are not real people +- Nutrition calculations run the same engine as production, on sandbox profiles +- `POST /v1/test/athletes/{athlete_id}/simulate-subscription` flips an athlete between tiers and fires the matching webhook. It exists only in sandbox +- Teaser subscribe links carry `test=1` so the whole checkout loop resolves against sandbox +- Rate limits are enforced by the same code path as production, from your account's configured limits. Sandbox is not exempt - No billing impact -## Security best practices +## Security practices -1. **Store keys in environment variables**, not in source code -2. **Use separate keys** for different services or deployment stages -3. **Rotate keys regularly** — at minimum every 90 days -4. **Monitor usage** for unexpected patterns -5. **Revoke immediately** if a key is exposed in logs, repos, or client code -6. **Never send keys over unencrypted channels** — all API traffic uses HTTPS +1. Store keys in environment variables, not in source code +2. Use separate keys for different services and deployment stages +3. Rotate on a schedule you can keep, and immediately on any suspected exposure +4. Watch `/v1/partner/usage` for patterns you cannot account for +5. Revoke immediately if a key appears in logs, repos, or client code ## Error responses -Authentication failures return a `401` status: +Authentication and authorization failures share the standard [error envelope](/error-handling): ```json { "error": { "type": "authentication_error", "code": "invalid_api_key", - "message": "The API key provided is invalid or has been revoked.", - "documentation_url": "https://api.saturday.fit/docs/authentication", + "message": "Invalid API key.", + "documentation_url": "https://docs.saturday.fit/errors#invalid_api_key", "request_id": "req_abc123def456" } } ``` -| Code | Meaning | -|------|---------| -| `invalid_api_key` | Key doesn't exist or has been revoked | -| `expired_api_key` | Key was rotated and the grace period has passed | -| `environment_mismatch` | Using a test key to access production data or vice versa | -| `missing_authorization` | No `Authorization` header provided | +| Code | Type | Status | Meaning | +|------|------|--------|---------| +| `missing_authorization` | `authentication_error` | 401 | No `Authorization` header | +| `invalid_api_key` | `authentication_error` | 401 | Key doesn't exist, or the header isn't `Bearer ` | +| `api_key_revoked` | `authentication_error` | 403 | Key revoked, or the account is suspended. For a self-serve account this usually means the subscription lapsed, and renewing reactivates the same key | +| `insufficient_scope` | `authorization_error` | 403 | The key's scopes don't permit this method | +| `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. diff --git a/coaching/become-a-coach.mdx b/coaching/become-a-coach.mdx index a81e64a..1530650 100644 --- a/coaching/become-a-coach.mdx +++ b/coaching/become-a-coach.mdx @@ -1,6 +1,6 @@ --- title: 'Become a coach' -description: 'Two ways to start coaching on Saturday — both land you in the same place' +description: 'Two ways to start coaching on Saturday, and the plan you land on' sidebarTitle: 'Become a coach' icon: 'user-plus' --- @@ -9,52 +9,61 @@ import { AppAction } from "/snippets/app-action.jsx" # Become a coach -There are two doors into coaching. Both end in exactly the same state: a coach account with **[portal access](https://coach.saturday.fit/dashboard)**, your own athlete subscription intact, an empty **[roster](https://coach.saturday.fit/dashboard)**, and a short activation checklist to get you going. +There are two doors into coaching. Both end in the same state: a coach account with **[portal access](https://coach.saturday.fit/dashboard)**, your own athlete subscription intact, an empty **[roster](https://coach.saturday.fit/dashboard)**, and an activation checklist on the dashboard. ## The two entry paths - Already a Saturday athlete? In the app, tap Become a Coach (Settings → Become a Coach, or tap the prompt on the home screen). Your existing account becomes a coach account — nothing about your own training changes. + Already a Saturday athlete? Open the **Accounts** page and tap Become a Coach. Your existing account gains coaching; your own training is untouched. **Stop Coaching** lives on the same page. - No Saturday account yet? Sign up at **[coach.saturday.fit](https://coach.saturday.fit)**. We provision an athlete account for you automatically so your coach identity and athlete identity are one account from day one. + No Saturday account yet? Sign up at **[coach.saturday.fit](https://coach.saturday.fit)**. Saturday provisions an athlete account for you, so your coach identity and athlete identity are one account from day one. + + The in-app **Become a Coach** row is behind a staged rollout, so it may not be on your Accounts page yet. Signing up at **[coach.saturday.fit](https://coach.saturday.fit)** works either way and reaches the same account. + + ## Every coach is also an athlete -This is true for every coach: **every coach has a Saturday athlete account and completes athlete onboarding.** Why — because you coach fueling best when you've felt it yourself, and because the persona toggle in the app lets you flip between coaching and your own training without logging out. +Every coach has a Saturday athlete account and completes athlete onboarding. You coach fueling better having used it, and the persona toggle on the Accounts page switches between **Coaching** and **My Training** without logging out. -- **Portal-first sign-up:** we auto-create your athlete account and subscription; you can finish athlete onboarding in-app whenever you like. -- **Already have an app account:** we keep it. Same login, now with coaching powers. -- **Signed up twice (app identity ≠ portal identity):** we detect the mismatch and guide you to link/merge the two into one account. This is what we recommend — you almost never want two. -- **Genuinely want them separate?** That's supported for power users, but the merge path is the recommended one. +- **Portal-first sign-up:** Saturday creates your athlete account and subscription. You can finish athlete onboarding in the app whenever you like. +- **Already have an app account:** it is kept. Same login, now with coaching. +- **Signed up twice (app identity does not match portal identity):** Saturday detects the mismatch and offers to link the two into one account, keeping the account with more training history. Linking is the recommended path. +- **Want them separate?** **Keep them separate** is offered alongside linking. A few account shapes cannot be linked automatically; in those cases Saturday's team completes the link by hand and you keep coaching in the meantime. ## Your activation checklist -Your empty dashboard shows a dismissible checklist so you're never staring at a blank screen: +An empty dashboard opens with a **Get started** card and four steps, each with its own button: -1. **Invite your first athlete** — from your **[roster](https://coach.saturday.fit/dashboard)**; see [Invite your first athlete](/coaching/first-athlete). -2. **Set your billing** — choose your plan and decide who pays in **[billing](https://coach.saturday.fit/admin/billing)**. See [Who pays](/coaching/billing/who-pays). -3. **Explore the coaching guide** — these docs. Start at [the dials](/coaching/nutrition/overview). +1. **Invite your first athlete** from your **[roster](https://coach.saturday.fit/dashboard)**. See [Invite your first athlete](/coaching/first-athlete). +2. **Set up billing**, which opens **[Coach Subscription](https://coach.saturday.fit/admin/subscription)** to pick a plan. See [Who pays](/coaching/billing/who-pays). +3. **Finish your athlete setup** in the app, about 5 minutes. The button shows a QR code that hands your own athlete onboarding to your phone. +4. **Read the coach docs**, which lands here. -Dismiss it any time; it won't nag. +The card tracks progress, collapses to a **Setup complete** strip when all four are done, and can be dismissed. Once dismissed, a **Setup guide** link reopens it. ## Choosing a plan -You don't have to pay to start. Pick or change your plan in **[billing](https://coach.saturday.fit/admin/billing)**. The plans: +You can start without paying. Plans live on **[Coach Subscription](https://coach.saturday.fit/admin/subscription)**, which the checklist's **Choose a plan** button, your Account Settings, and the command palette all open. (The **$ Earning** item in the nav is Stripe Connect payouts, not your plan.) + +| Plan | Price | Athletes | Assistants | +|------|-------|----------|------------| +| **Coach** | Free | 1, who self-pays | None | +| **Pro Coach** | $12.99/mo | 2 included | 1 | +| **Head Coach** | $49.99/mo | 5 included | Up to 5 | +| **Business** | $149/mo flat | Fair-use roster ceiling | Unlimited | +| **Enterprise** | Custom | Custom | Unlimited | + +**Included** means covered by your plan fee, so the athlete pays nothing. The free **Coach** tier is the exception: it lets you coach one athlete, but that athlete pays for their own Saturday subscription. Covering an athlete yourself starts at Pro Coach. See [Who pays](/coaching/billing/who-pays). -| Plan | Price | Included athletes | Assistants | -|------|-------|-------------------|------------| -| **Free** | $0 | 1 | — | -| **Pro Coach** | $12.99/mo | 2 | +1 | -| **Head Coach** | $49.99/mo | 5 | +5 | -| **Business** | $149/mo flat | fair-use | unlimited | -| **Enterprise** | Custom | Custom | Custom | +Beyond the assistants your plan includes, you can buy extra assistant seats at $10/seat/month and release them at any time. Business and Enterprise are already unlimited, so the seat add-on does not apply to them. - **Saturday Lifetime owner?** Your coaching plan is **50% off, forever** — Pro Coach is $6.50/mo, Head Coach is $25.00/mo. The discount applies automatically; you'll see it on the plan cards. + **Saturday Lifetime owner?** Your coaching plan is 50% off for as long as you hold it. Pro Coach is $6.50/mo and Head Coach is $25.00/mo. The discount applies automatically and the plan cards show a **Lifetime 50% off** badge. -Plans are monthly (no annual coach plans). You can upgrade, downgrade, or cancel any time from your **[billing](https://coach.saturday.fit/admin/billing)** page — see [Take over & relinquish](/coaching/billing/take-over-relinquish) for what happens to your athletes when your plan changes. +Coach plans are billed monthly. Upgrade, downgrade, or cancel from **[Coach Subscription](https://coach.saturday.fit/admin/subscription)**. An upgrade takes effect immediately; a downgrade or cancellation takes effect at the end of your paid period, and both can be undone before then. Cancelling moves your covered athletes to self-pay on that date. See [Take over & relinquish](/coaching/billing/take-over-relinquish) for what your athletes see when that happens. diff --git a/coaching/billing/coach-paid-athletes.mdx b/coaching/billing/coach-paid-athletes.mdx index a2d7ba8..e83ae55 100644 --- a/coaching/billing/coach-paid-athletes.mdx +++ b/coaching/billing/coach-paid-athletes.mdx @@ -1,47 +1,51 @@ --- title: 'Coach-paid athletes' -description: 'Cover an athlete beyond your included seats — what it costs and how it works' +description: 'Cover an athlete beyond your included seats: what it costs and how it is billed' sidebarTitle: 'Coach-paid athletes' icon: 'hand-holding-heart' --- # Coach-paid athletes -A **coach-paid athlete** is an athlete whose Saturday subscription **you** cover. It's how you take fueling entirely off an athlete's shoulders — they get full Saturday with nothing to set up or pay. You manage who you cover from your **[billing](https://coach.saturday.fit/admin/billing)** page. +A **coach-paid athlete** is an athlete whose Saturday subscription you cover. They get Saturday with nothing to set up or pay. You start and stop coverage per athlete from your **[roster](https://coach.saturday.fit/dashboard)**. ## When an athlete is coach-paid -- **Within your included seats:** athletes who fit your plan's included seats are covered for **$0** beyond your monthly plan fee. -- **Beyond your included seats:** each additional athlete you choose to cover is a flat per-athlete monthly add-on, roughly a standard Saturday subscription price — **with a volume discount** as your covered count grows (see [The volume discount](/coaching/billing/volume-discount)). +- **Within your included seats:** athletes who fit your plan's included seats cost you $0 beyond your monthly plan fee. These are labelled **Included** on the roster, not coach-paid. +- **Beyond your included seats:** each additional athlete you cover is a per-athlete monthly add-on at the standard Saturday athlete subscription price, **$12.99/mo**, reduced by your volume discount once you cover enough of them. See [The volume discount](/coaching/billing/volume-discount). -You always choose per athlete from your **[billing](https://coach.saturday.fit/admin/billing)** page: cover them, or let them self-pay. Inviting an athlete never auto-charges you. +Covering athletes beyond your included seats requires a paid plan. On the free **Coach** plan the portal declines the action and points you at an upgrade. ## How it's priced -Your coach-paid athletes are billed as **add-on items on your existing coaching subscription** — one subscription, one invoice, one payment method, all on your **[billing](https://coach.saturday.fit/admin/billing)** page. There's no separate bill to manage. +Coach-paid athletes are billed as a single per-unit line item on your existing coaching subscription: one subscription, one invoice, one payment method. The quantity is your coach-paid athlete count and the unit price is $12.99 less your volume discount. -The per-athlete price is the standard add-on rate, **discounted by your volume tier**. Your volume tier is keyed off your total **coach-paid athlete count** — that's your included-seat athletes **plus** your coach-paid extras. As that count crosses a threshold, the discount applies to the add-on price automatically. See [The volume discount](/coaching/billing/volume-discount) for the full curve. +Your volume tier is keyed off your **coach-paid athlete count**, the athletes you cover beyond your included seats. That is the same number the roster's seat breakdown shows next to **Coach-paid athletes**, alongside the discount it earns. Included-seat athletes are already covered by your plan fee and are counted separately. See [The volume discount](/coaching/billing/volume-discount) for the full curve. ## Covering and uncovering -You can start or stop covering an athlete from your **[billing](https://coach.saturday.fit/admin/billing)** page or inline on your **[roster](https://coach.saturday.fit/dashboard)**: +From an athlete's row menu or their billing drawer on your **[roster](https://coach.saturday.fit/dashboard)**: -- **Cover** an athlete → they become coach-paid; if they were self-paying, their old payment is canceled first (no double-charge). -- **Uncover** (relinquish) → coverage moves to the athlete on a friendly handoff. +- **Cover** makes them coach-paid. If they were already paying for Saturday, that payment is stopped first so the same period is never billed twice. +- **Stop covering** (relinquish) hands payment back to the athlete over a grace window. -Both flows are covered in [Take over & relinquish](/coaching/billing/take-over-relinquish), including the billing-impact preview that shows exactly what changes before you confirm. +Both flows are covered in [Take over & relinquish](/coaching/billing/take-over-relinquish), including the billing-impact preview that shows what changes before you confirm. -## At trial-end + + One case Saturday cannot stop for you: if the athlete pays for Saturday through the App Store or Google Play, that subscription is managed by Apple or Google and **the athlete has to cancel it themselves**. Saturday tells you when this applies at the moment you cover them, so you can ask them to do it. + -When a coach-created athlete's trial ends, Saturday keeps them whole automatically: +## At trial end -- **You have a free seat:** they're seamlessly covered from your plan — no action needed (you'll get a friendly "now covering Sam" note). -- **Your seats are full:** you're prompted on your **[billing](https://coach.saturday.fit/admin/billing)** page to either cover them as a coach-paid athlete or send them a zero-friction self-pay handoff. +When a coach-created athlete's trial ends: -Either way, the athlete's coverage transitions without a gap. +- **You have a free seat:** they move onto it automatically, at $0, and you get a "now covering Sam" note. +- **Your seats are full:** you get an alert to decide, and the athlete is immediately shown that their membership is ending, with a 14-day grace window, a one-time 20%-off annual offer, and an emailed link to take over. You can still cover them during that window. + +Either path keeps the athlete's access continuous; nobody is dropped without notice. ## See also -- [The volume discount](/coaching/billing/volume-discount) — your per-athlete price as you scale. -- [Take over & relinquish](/coaching/billing/take-over-relinquish) — the start/stop flows. -- [Who pays](/coaching/billing/who-pays) — how coach-paid fits among all the modes. +- [The volume discount](/coaching/billing/volume-discount), your per-athlete price as you scale. +- [Take over & relinquish](/coaching/billing/take-over-relinquish), the start and stop flows. +- [Who pays](/coaching/billing/who-pays), how coach-paid fits among all the modes. diff --git a/coaching/billing/take-over-relinquish.mdx b/coaching/billing/take-over-relinquish.mdx index 66e4cb8..c7a22b7 100644 --- a/coaching/billing/take-over-relinquish.mdx +++ b/coaching/billing/take-over-relinquish.mdx @@ -1,61 +1,66 @@ --- title: 'Take over & relinquish' -description: 'Start covering an athlete, or hand payment back — always clear, always reversible, never a surprise' +description: 'Start covering an athlete, or hand payment back: what each side sees and what it costs' sidebarTitle: 'Take over & relinquish' icon: 'arrow-right-arrow-left' --- # Take over & relinquish -Coverage moves in two directions: you can **take over** an athlete's payments (start covering them), or **relinquish** them (hand payment back). You do both from your **[billing](https://coach.saturday.fit/admin/billing)** page. Both are fully previewed and reversible. +Coverage moves in two directions. You can **take over** an athlete's payments, or **relinquish** them and hand payment back. You do both from the athlete's row menu or billing drawer on your **[roster](https://coach.saturday.fit/dashboard)**, and both show their consequences before you confirm. ## See it before you do it -Any coverage change on your **[billing](https://coach.saturday.fit/admin/billing)** page shows a **billing-impact preview** before you confirm. The preview tells you: +Every coverage change opens a **billing-impact preview**. It tells you: -- The **$ that stops** and the **paid-through date**. -- **Who pays next** (you, or the athlete on takeover) and that the athlete will be notified. -- The **refund/proration** state. -- **Reversibility** — whether you can undo or re-cover within the grace window. -- Reassuring terms for the athlete: access continues through end-of-period **plus a 14-day grace**. -- That there are **multiple easy ways to pay** and a one-time **20%-off annual** offer. +- The amount that stops and the paid-through date. +- Who pays next, and that the athlete will be notified. +- The refund and proration state. +- Whether you can undo, and how long you can re-cover. +- What the athlete gets: access through the end of the paid period plus a 14-day grace, a one-time 20%-off annual offer, and several ways to pay. ## Take over payments -From your **[billing](https://coach.saturday.fit/admin/billing)** page or your **[roster](https://coach.saturday.fit/dashboard)**, choose **Take over payments**. What happens depends on how the athlete was paying: +Use **Cover** from the athlete's row menu, or **Take over** in their billing drawer. Both buttons carry the athlete's name. What happens next depends on how the athlete was paying: -- **Self-paying monthly:** their subscription is canceled at period end; your coverage begins seamlessly so there's **never a double-charge**. -- **Self-paying annually:** their subscription is canceled with a **prorated refund**, and your coverage begins now. -- **In a trial:** your coverage simply takes over — no gap. +- **Self-paying monthly:** their subscription is cancelled at period end and your coverage begins there, so the same period is never billed twice. Your add-on starts on your next cycle rather than immediately. +- **Self-paying annually:** their subscription is cancelled with a prorated refund and your coverage begins now, so your add-on bills now. +- **In a trial:** your coverage takes over with no gap. +- **Paying through the App Store or Google Play:** Apple and Google own that subscription and Saturday cannot cancel it. Your coverage starts, and the portal tells you the athlete has to cancel the store subscription themselves to stop paying. -The athlete gets a good-news notification: **"Your coach is now covering your Saturday membership."** +If Stripe fails partway, the cover is aborted rather than allowed to double-charge. + +The athlete gets a push notification naming you as the coach now covering their membership. ## Relinquish (hand payment back) -When you stop covering an athlete from your **[billing](https://coach.saturday.fit/admin/billing)** page — by choice, on downgrade, or when you stop coaching — the athlete becomes payment-responsible: +When you stop covering an athlete, by choice, on a downgrade, or when you stop coaching, the athlete becomes payment-responsible: -1. The athlete keeps access through the **end of the current paid period plus a 14-day grace**. -2. They get a friendly notification and a persistent **"Your subscription is ending"** screen with a countdown and a clear take-over button. -3. They're offered a one-time, grace-window **20%-off annual** deal — "multiple easy ways to pay." -4. If they're an **unlinked self-payer**, the message is purely about unlinking — no payment talk at all. +1. Your charge stops. You are paid through the current period, with no mid-cycle charge and no refund for the remainder. +2. The athlete keeps access through the end of that paid period plus a **14-day grace**. +3. They get a notification ("Your membership is ending soon") and a persistent screen with a countdown and a take-over button. +4. They are offered a one-time 20%-off annual deal, valid for the grace window, and emailed a secure link to redeem it. +5. If they are an unlinked self-payer, the message is about unlinking only, with no payment talk. - **The 14-day grace is real.** Even after the paid period ends, an athlete keeps Saturday for 14 more days. Plenty of room for them to take over without ever losing access. + The 14-day grace runs *after* the paid period ends. An athlete keeps Saturday for those 14 extra days, which is the window in which they can take over without ever losing access. ## Undo and reversibility -Both takeover and relinquish are reversible within the grace window — from your **[billing](https://coach.saturday.fit/admin/billing)** page you can **re-cover** an athlete you just relinquished, and a fresh relinquish offers an immediate undo. Side effects (like the athlete notification) are held through the undo window so an accidental click doesn't fire anything you'd regret. +A fresh relinquish offers an immediate undo for a few seconds. After that window the athlete's grace has started and the undo is replaced by a **Re-cover** button in their billing drawer, available any time until the grace ends. Side effects such as the athlete notification are held through the undo window, so a mis-click does not fire anything. -## Org-paid athletes (assistants, note) +## Org-paid athletes -If you're an **assistant coach**, you can't end an **org-paid** arrangement unless your head coach or org owner has explicitly granted you that permission from the **[team](https://coach.saturday.fit/dashboard/team)** page. See [Permissions](/coaching/team/permissions). +If you are an assistant coach, you cannot end an **org-paid** arrangement unless an org admin has granted you that permission explicitly. It is a separate grant from the general billing-coverage delegation, so being able to cover and uncover your own athletes does not carry it with it. Org admins manage both on the **Access & Roles** page, where role assignment and assistant delegation are available on every plan. See [Permissions](/coaching/team/permissions). ## When you cancel your plan or stop coaching -Canceling your coaching plan from your **[billing](https://coach.saturday.fit/admin/billing)** page, or offboarding entirely, routes **every coach-paid athlete** through this same relinquish flow — 14-day grace, friendly notification, and the 20%-annual offer. Self-pay athletes simply unlink. Nobody is left stranded. You can also issue refunds directly from your **[billing](https://coach.saturday.fit/admin/billing)** page (see [Who pays](/coaching/billing/who-pays)). +Cancelling your coaching plan from **[Coach Subscription](https://coach.saturday.fit/admin/subscription)** takes effect at the end of your paid period, and your covered athletes move to self-pay on that date through this same relinquish flow: 14-day grace, notification, and the 20%-off annual offer. Self-pay athletes unlink. You can undo the cancellation before its effective date. + +Separately, if you bill athletes a coaching fee through Stripe Connect, you can refund one of those charges from the **$ Earning** page. That refunds your coaching fee, not the athlete's Saturday subscription. ## See also -- [Coach-paid athletes](/coaching/billing/coach-paid-athletes) — what coverage costs. -- [Who pays](/coaching/billing/who-pays) — all the billing modes. +- [Coach-paid athletes](/coaching/billing/coach-paid-athletes), what coverage costs. +- [Who pays](/coaching/billing/who-pays), all the billing modes. diff --git a/coaching/billing/volume-discount.mdx b/coaching/billing/volume-discount.mdx index d40bc21..524564d 100644 --- a/coaching/billing/volume-discount.mdx +++ b/coaching/billing/volume-discount.mdx @@ -1,50 +1,45 @@ --- title: 'The volume discount' -description: 'The more athletes you cover, the less each one costs — the full discount curve' +description: 'The per-athlete price drops as your coach-paid count grows: the full curve' sidebarTitle: 'Volume discount' icon: 'percent' --- # The volume discount -When you cover athletes as [coach-paid athletes](/coaching/billing/coach-paid-athletes) from your **[billing](https://coach.saturday.fit/admin/billing)** page, the per-athlete price drops as your covered count grows. Cover more, pay less per head — automatically. +When you cover athletes as [coach-paid athletes](/coaching/billing/coach-paid-athletes), the per-athlete price drops as your coach-paid count grows. The base price is the standard Saturday athlete subscription, $12.99/mo, and the discount comes off that. ## The curve -The discount is keyed off your **coach-paid athlete count** — your included-seat athletes **plus** your coach-paid extras. As that count crosses a threshold, the discount applies to every coach-paid add-on on your **[billing](https://coach.saturday.fit/admin/billing)** page: +| Coach-paid athletes | Discount | Price per athlete | +|---------------------|----------|-------------------| +| 1–9 | 0% | $12.99 | +| 10–19 | 5% | $12.35 | +| 20–49 | 10% | $11.70 | +| 50–99 | 15% | $11.05 | +| 100–199 | 20% | $10.40 | +| 200–299 | 30% | $9.10 | +| 300–599 | 40% | $7.80 | +| 600+ | 50% | $6.50 | -| Coach-paid athletes | Discount per athlete | -|---------------------|----------------------| -| 1–9 | 0% | -| 10–19 | **5%** | -| 20–49 | **10%** | -| 50–99 | **15%** | -| 100–199 | **20%** | -| 200–299 | **30%** | -| 300–599 | **40%** | -| 600+ | **50%** | - - - At **10+** coach-paid athletes you start saving (5%), and the savings climb all the way to **50% at 600+**. The discount applies to the coach-paid add-on price — your plan's included seats are already covered by your plan fee. - +Crossing a threshold reprices every coach-paid athlete on your invoice. **[Coach Subscription](https://coach.saturday.fit/admin/subscription)** plots the same curve. ## How it's counted -- The count is your **total coach-paid athletes**: athletes in your included seats **plus** the extras you've chosen to cover. -- When your count crosses a threshold, the discounted unit price is **re-stamped on your next billing cycle**, visible on your **[billing](https://coach.saturday.fit/admin/billing)** page — you don't have to do anything. +- The count is your **coach-paid athletes**: the athletes you cover beyond your plan's included seats. Included-seat athletes are already covered by your plan fee, your own subscription never counts, and athletes who pay for themselves never count. +- Your discount is set at the start of each billing cycle and stays uniform across all your coach-paid athletes for that cycle. +- The roster's seat breakdown shows the current count, the discount it earns, and what covering one more would cost. - The same curve applies to **org-paid** athletes, counted against the organization. ## Worked example -Say you're on **Head Coach** (5 included seats) and you cover **17 athletes** total: - -- The first 5 are covered by your $49.99/mo plan fee. -- The remaining 12 are coach-paid add-ons. -- Your total coach-paid count is **17**, which lands in the **10–19** tier → a **5% discount** on each coach-paid add-on. +You are on **Head Coach**, which includes 5 seats, and you cover **17 athletes** in total: -Cross into 20 total and every add-on jumps to the **10%** tier. You can see this breakdown on your **[billing](https://coach.saturday.fit/admin/billing)** page. +- The first 5 sit on included seats, covered by your $49.99/mo plan fee. +- The remaining **12** are coach-paid add-ons. Twelve lands in the 10–19 bracket, so each is discounted 5%, at $12.35/mo. +- Covering 8 more takes your coach-paid count to 20, and every coach-paid athlete moves to the 10% bracket at $11.70/mo. ## See also -- [Coach-paid athletes](/coaching/billing/coach-paid-athletes) — how add-on pricing works. -- [Who pays](/coaching/billing/who-pays) — the billing modes overview. +- [Coach-paid athletes](/coaching/billing/coach-paid-athletes), how add-on pricing works. +- [Who pays](/coaching/billing/who-pays), the billing modes overview. diff --git a/coaching/billing/who-pays.mdx b/coaching/billing/who-pays.mdx index 97064ae..c408c90 100644 --- a/coaching/billing/who-pays.mdx +++ b/coaching/billing/who-pays.mdx @@ -1,50 +1,60 @@ --- title: 'Who pays' -description: 'The billing modes — coach-paid, athlete-paid, org-paid, and tier-bundled — in plain language' +description: 'The billing modes: self-pay, included seat, coach-paid, and org-paid' sidebarTitle: 'Who pays' icon: 'credit-card' --- # Who pays -Every athlete on your **[roster](https://coach.saturday.fit/dashboard)** has a Saturday subscription, and for every athlete it's always clear **who's paying for it**. You set all of this from your **[billing](https://coach.saturday.fit/admin/billing)** page. There's no ambiguity, no double-charging, and no athlete ever gets billed for something you're covering. +Every athlete on your **[roster](https://coach.saturday.fit/dashboard)** has a Saturday subscription, and every athlete has one payer. The roster's **Who pays** column shows which, and you can filter the roster by it. -Two questions decide the picture, and they're independent of each other: +Two questions decide the picture, and they are independent of each other: -1. **Who pays for the athlete's Saturday subscription?** (you, them, or your organization) -2. **Is the athlete linked to you?** (on your roster or not) +1. **Who pays for the athlete's Saturday subscription?** You, them, or your organization. +2. **Is the athlete linked to you?** On your roster or not. -All combinations are valid — you can coach an athlete who pays for themselves, or cover an athlete you haven't even linked yet. This page is about question 1. +All combinations are valid. You can coach an athlete who pays for themselves, or cover an athlete you have not linked yet. This page is about question 1. ## The payment modes -Choose and review each athlete's mode from your **[billing](https://coach.saturday.fit/admin/billing)** page. - | Mode | Who pays the athlete's Saturday sub | When it applies | |------|-------------------------------------|-----------------| -| **Athlete self-pays** | The athlete | Default. They have (or get) their own subscription. | -| **Coach-paid athlete** | You | You choose to cover them — see [Coach-paid athletes](/coaching/billing/coach-paid-athletes). | -| **Tier-bundled** | Your plan | Athletes within your plan's included seats are covered for $0. | -| **Org-paid** | Your organization | A Business/Enterprise org covers a linked athlete's sub. | +| **Athlete self-pays** | The athlete | The default. They have, or get, their own subscription. | +| **Included seat** | Your plan fee | The athlete fits within your plan's included seats, so they pay $0. Pro Coach and above. | +| **Coach-paid athlete** | You | You cover them beyond your included seats. See [Coach-paid athletes](/coaching/billing/coach-paid-athletes). | +| **Org-paid** | Your organization | An organization you administer covers a linked athlete's sub. | - **Mixed billing is normal.** On the same roster, some athletes can be coach-paid while others self-pay. You decide per athlete — there's no all-or-nothing. + **Mixed billing is normal.** On the same roster, some athletes can be coach-paid while others self-pay. You decide per athlete. ## Included seats come first -Your plan includes a number of athlete seats (Free: 1 · Pro Coach: 2 · Head Coach: 5 · Business: fair-use). Athletes who fit within your included seats are **covered free by your plan price** — they pay $0, and so do you beyond your monthly plan fee. +Your plan includes a number of athlete seats: Pro Coach 2, Head Coach 5, Business and Enterprise a fair-use ceiling. Athletes who fit within those seats are covered by your plan fee and pay $0 themselves. + +The free **Coach** plan is the exception. It lets you coach one athlete, but that athlete pays for their own Saturday subscription; the free plan funds no seats and cannot cover an athlete. Covering athletes starts at Pro Coach. + +Beyond your included seats you choose per athlete: **cover them**, which makes them a [coach-paid athlete](/coaching/billing/coach-paid-athletes), or let them self-pay. + +Org-paid works the same way with the organization as the payer. It is not tied to a particular plan tier. What it needs is an organization you administer, the billing-coverage permission, and, for any athlete beyond the organization's included seats, a paid plan on the organization itself. Ending an org-paid arrangement takes a separate permission that an org admin grants explicitly. -Beyond your included seats, you choose per athlete from your **[billing](https://coach.saturday.fit/admin/billing)** page: **cover them** (they become a [coach-paid athlete](/coaching/billing/coach-paid-athletes)) or **let them self-pay**. +## Where you set it + +Coverage is decided per athlete, not in one global setting. From your **[roster](https://coach.saturday.fit/dashboard)**, use an athlete's row menu or open their billing drawer to cover them or stop covering them. Selecting several athletes gives you **Cover** and **Uncover** for the whole selection, with the cost shown before you confirm. + +Your own plan, the volume-discount curve, and your plan's seat allotment live on **[Coach Subscription](https://coach.saturday.fit/admin/subscription)**. ## Your own subscription -If you're on a paid plan, your plan covers **your own** athlete subscription too — you never pay twice. (On the Free plan, you self-pay your own subscription like any athlete; "yourself" isn't a free roster seat.) Review or change your plan from your **[billing](https://coach.saturday.fit/admin/billing)** page. +On a paid plan, your plan also covers your own athlete subscription, and it never consumes one of your athlete seats. On the free Coach plan you pay for your own subscription like any athlete. + +If your coaching plan's payment fails, you keep your tier for a 14-day grace window rather than losing it immediately. -No silent lapse — an athlete's access never just disappears. See [Take over & relinquish](/coaching/billing/take-over-relinquish) for how coverage changes, grace windows, and double-charge protection work. +An athlete's access never disappears without warning. See [Take over & relinquish](/coaching/billing/take-over-relinquish) for how coverage changes, grace windows, and double-charge protection work. ## See also -- [Coach-paid athletes](/coaching/billing/coach-paid-athletes) — how covering an athlete is priced. -- [The volume discount](/coaching/billing/volume-discount) — the more you cover, the less each costs. -- [Take over & relinquish](/coaching/billing/take-over-relinquish) — starting and stopping coverage. +- [Coach-paid athletes](/coaching/billing/coach-paid-athletes), how covering an athlete is priced. +- [The volume discount](/coaching/billing/volume-discount), your per-athlete price as you cover more. +- [Take over & relinquish](/coaching/billing/take-over-relinquish), starting and stopping coverage. diff --git a/coaching/first-athlete.mdx b/coaching/first-athlete.mdx index 5acbceb..a19d893 100644 --- a/coaching/first-athlete.mdx +++ b/coaching/first-athlete.mdx @@ -1,57 +1,62 @@ --- title: 'Invite your first athlete' -description: 'Invite, create, or bulk-import athletes — and what happens when they accept' +description: 'Invite, create, or bulk-import athletes, and what happens when they accept' sidebarTitle: 'Your first athlete' icon: 'paper-plane' --- # Invite your first athlete -Your **[roster](https://coach.saturday.fit/dashboard)** starts empty. Here's how to fill it — one athlete, a paste of many, or a CSV. +Your **[roster](https://coach.saturday.fit/dashboard)** starts empty. You can fill it one athlete at a time, from a pasted block of emails, or from a CSV. Another coach can also transfer an athlete to you. ## Invite one athlete -From your **[roster](https://coach.saturday.fit/dashboard)**, click **Invite** → **One athlete**. Enter their email (and optionally their name, which personalizes the invite). They get a branded email with a link to accept. +From your **[roster](https://coach.saturday.fit/dashboard)**, click **Invite**, then the **One athlete** tab. Enter their email and, optionally, their name, which personalizes the invite. Saturday emails them a link and also gives you a shareable invite link you can send yourself. -When they accept, they choose to share their Saturday data with you, and they appear on your **[roster](https://coach.saturday.fit/dashboard)**. If they don't have a Saturday account yet, the invite walks them through creating one. +By default the invite says nothing about who pays: *"This athlete will be self-pay unless you cover them after they accept."* You can instead tick **I'll cover this athlete's Saturday subscription (coach-paid)** before sending. That commits you: when they accept, their billing switches to coach-paid, and the modal shows the estimated monthly cost first. If they already pay for Saturday, their existing plan is cancelled and the unused portion refunded. Lifetime members cost you nothing. The athlete sees the change before they accept. + +When they accept, they choose to share their Saturday data with you and they appear on your **[roster](https://coach.saturday.fit/dashboard)**. If they have no Saturday account yet, the invite walks them through creating one. - **Consent is built in.** An athlete always explicitly accepts coaching and agrees to share their data. You never silently gain access to someone's account. + **Consent is built in.** An athlete explicitly accepts coaching and agrees to the requested permissions. They can also decline. You never silently gain access to someone's account. ## Invite many at once -From your **[roster](https://coach.saturday.fit/dashboard)**, click **Invite** → **Many (paste or CSV)**. +From your **[roster](https://coach.saturday.fit/dashboard)**, click **Invite**, then the **Many (paste or CSV)** tab. + +- **Paste:** drop in a block of emails separated by commas, spaces, or newlines, mixed however they came. Saturday sorts them as you type. Valid addresses are marked for sending, duplicates collapse, anything that isn't an email is flagged for a quick fix, and anyone already on your roster is skipped. A running line counts all four: valid, duplicates, invalid, and already on roster. +- **CSV:** drag in a `.csv` or click to choose one. Saturday detects the email column and an optional name column; if it is ambiguous, you pick the column. Very large files are rejected with a nudge to paste instead. -- **Paste tab:** drop in a block of emails — commas, spaces, or one per line, mixed however. Saturday sorts them out as you type: valid emails turn green, duplicates collapse, typos get flagged for a quick fix, and anyone already on your roster is skipped. A running count shows "42 valid · 3 duplicates · 1 invalid." -- **CSV tab:** drag in a `.csv`. Saturday auto-detects the email column (and an optional name column to personalize each invite). If it's ambiguous, you pick the column. +Before sending you see the seat impact: your plan's included seats cover some of the batch, and the rest are self-pay unless you cover them. A **Cover all invited athletes (coach-paid)** toggle commits to covering the whole batch. Leave it off and no invite charges you; you decide coverage per athlete after each one accepts. -Before sending, you'll see the **seat impact** — for example, "Inviting 42 athletes. Your plan includes 5 seats; the rest will be self-pay unless you cover them." Invites never auto-charge you; you decide coverage per athlete after they accept. +## Set an athlete up for them -## Create / set up an athlete for them +Coaching a less-techy athlete? You can build their fueling **[Setup](/coaching/nutrition/setup-dials)** (bottles, mix, ratio) before they ever open the app, then use **Invite to claim their account** from the athlete's row menu. Saturday emails them a link to take ownership. They keep everything already there, their activities, products, and fueling plan, and you stay their coach. Nothing changes for you until they accept. -Coaching a less-techy athlete? From **[athlete management](https://coach.saturday.fit/dashboard?view=manage)** you can **set up their fueling on their behalf** — pre-fill their [Setup](/coaching/nutrition/setup-dials) (bottles, mix, ratio) before they ever open the app — then hand off a one-tap "claim your account" invite. They arrive to a working configuration instead of a blank slate. +## Manage invites and relationships -## Manage pending invites +| Action | Where | What it does | +|--------|-------|--------------| +| **Cancel invite** | The pending athlete's row on your **[roster](https://coach.saturday.fit/dashboard)** | Withdraws an invite before it's accepted | +| **Re-invite** | The **Invite** modal, same email | Sends the invitation again. A cooldown blocks rapid repeats, and an invitation that has expired needs a fresh one | +| **Transfer or share** | Row menu, for an athlete already on your roster | Moves the athlete to another coach, or adds a second coach who can see their training | +| **Remove from roster** | Row menu | Unlinks the athlete. They stay a Saturday user; you lose access to their fueling and can re-invite later | -Do this from **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. +An athlete awaiting acceptance carries a **Pending** badge on the roster. -| Action | What it does | -|--------|--------------| -| **Resend** | Re-send an invite that hasn't been accepted yet | -| **Cancel / revoke** | Withdraw an invite before it's accepted | -| **Transfer** | Hand off an athlete to (or from) another coach — the athlete consents | +**Transfer** hands the athlete to another coach: your coverage ends at period end plus a 14-day grace, and the new coach can cover them or the athlete can take over. **Share** adds a viewer-coach who can see the athlete's training while you stay primary and keep handling their membership. Both wait on the receiving coach accepting the handoff, and the handoff link expires after 7 days. ## After they accept Once an athlete is on your **[roster](https://coach.saturday.fit/dashboard)** you can: -- See their fueling history, adherence, and flags — see [Flags & groups](/coaching/roster/flags-and-groups). -- Read their AI fueling report — see [AI report](/coaching/roster/ai-report). -- View and adjust their **Setup** — see [The Setup](/coaching/nutrition/setup-dials). -- Decide who pays for their Saturday subscription in **[billing](https://coach.saturday.fit/admin/billing)** — see [Who pays](/coaching/billing/who-pays). -- **[Message](https://coach.saturday.fit/dashboard/messages)** them, or add them to a group from **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. +- See their fueling history, adherence, and flags. See [Flags & groups](/coaching/roster/flags-and-groups). +- Read their AI fueling report. See [AI report](/coaching/roster/ai-report). +- View and adjust their **Setup**. See [The Setup](/coaching/nutrition/setup-dials). +- Decide who pays for their Saturday subscription from their row. See [Who pays](/coaching/billing/who-pays). +- **[Message](https://coach.saturday.fit/dashboard/messages)** them, or add them to a group. - **The fastest first win:** invite an athlete from your **[roster](https://coach.saturday.fit/dashboard)**, open their AI report, and use "Apply this plan to their Setup." + A quick first pass: invite an athlete, open their fueling plan, and use **Apply this plan to Setup**. diff --git a/coaching/nutrition/bottling-fill-mix.mdx b/coaching/nutrition/bottling-fill-mix.mdx index a7394ea..4637158 100644 --- a/coaching/nutrition/bottling-fill-mix.mdx +++ b/coaching/nutrition/bottling-fill-mix.mdx @@ -1,30 +1,40 @@ --- -title: 'Bottling — fill & mix' -description: 'How Saturday turns per-hour targets into real bottles (carriage → consumption)' -sidebarTitle: 'Bottling — fill & mix' +title: 'Bottling: fill & mix' +description: 'How Saturday turns per-hour targets into real bottles' +sidebarTitle: 'Bottling: fill & mix' icon: 'bottle-water' --- -# Bottling — fill & mix +# Bottling: fill & mix
-**What it is.** An athlete's **Setup** describes the bottles and flasks they carry — their **carriage** — and how each one is filled and mixed. Saturday maps the per-hour prescription onto that carriage so the athlete knows exactly what to put in each bottle and when to drink it. That's the whole journey: **carriage → consumption.** Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to see their carriage. +**What it is.** An athlete's Setup describes the vessels they carry, their **carriage**, and how each one is filled and mixed. Saturday maps the per-hour prescription onto that carriage, so the athlete knows what goes in each bottle and when to drink it. -**When to turn it.** When the athlete's gear changes (more or fewer bottles, a bigger flask, a feed zone on course), when a too-concentrated mix is causing stomach trouble, or when what they carry doesn't cover the session's duration. Make the change from their Setup — open the athlete from your **[roster](https://coach.saturday.fit/dashboard)**. +**When to change it.** When the athlete's gear changes, such as more or fewer bottles, a bigger flask, or a feed zone on course. When a mix that is too concentrated is causing stomach trouble. When what they carry does not cover the session's duration. -**What changes for the athlete.** A concrete bottle plan — fill volumes, scoop counts, and a drink cadence — with none of the math left to them. They just drink the plan. +**What the athlete gets.** A concrete bottle plan with fill volumes, scoop counts, and a drink cadence, with none of the math left to them. -**The question it answers.** *"How much do I actually put in each bottle, and how often do I drink?"* +**The question it answers.** *"How much do I put in each bottle, and how often do I drink?"* + +## How carriage is structured + +Carriage is stored per activity type, not globally. A cycling Setup and a running Setup are separate, each an ordered list of **slots**. A slot is one vessel with a maximum volume. + +So "how many bottles" is the slot count for that activity type, and "how big" is each slot's volume ceiling. + + + **There is no concentration dial.** Concentration is an outcome, not an input. You set what the athlete carries and how large each vessel is; Saturday works out how much carbohydrate and sodium go into each one to hit the prescription. To make a mix less concentrated, give the athlete more fluid capacity to spread the same carbohydrate across. + ## What you do as a coach -- **Read it first.** Open the athlete's current carriage from your **[roster](https://coach.saturday.fit/dashboard)** before changing anything — see ground truth, then adjust. -- **Adjust the count and concentration.** Tap Adjust Setup to change how many bottles they carry and how concentrated each is. Saturday re-maps the consumption plan and shows a live impact sentence. ([How the Setup works](/coaching/nutrition/setup-dials).) -- **Pre-fill for a new athlete.** Setting up a less-techy athlete? From your **[roster](https://coach.saturday.fit/dashboard)**, open them and dial in their bottling before they ever open the app — they arrive to a working plan (assisted onboarding). +- **Read it first.** Open the athlete's current carriage before changing anything, so you are adjusting from ground truth rather than a guess. +- **Edit the slots.** Pick the activity type, then add, remove, or resize slots. Saturday re-maps the consumption plan and shows a one-line impact preview as you go. See [The Setup](/coaching/nutrition/setup-dials). +- **Pre-fill for a new athlete.** For an athlete who is less comfortable with the app, you can dial in their bottling from your roster before they ever open it, so they arrive to a working plan. ## See also -- [The Setup](/coaching/nutrition/setup-dials) — where carriage and mix live and how to adjust them. -- [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) — what goes *into* the mix. -- [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) — what you fill bottles with. +- [The Setup](/coaching/nutrition/setup-dials) for where carriage lives and how to adjust it. +- [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) for what goes into the mix. +- [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) for what you fill bottles with. diff --git a/coaching/nutrition/eco-mode-vs-products.mdx b/coaching/nutrition/eco-mode-vs-products.mdx index 7a6c581..765405e 100644 --- a/coaching/nutrition/eco-mode-vs-products.mdx +++ b/coaching/nutrition/eco-mode-vs-products.mdx @@ -1,6 +1,6 @@ --- title: 'Eco-mode vs products' -description: 'Two ways to hit the same targets — mix your own from staples, or use branded products' +description: 'Two ways to hit the same targets: mix from staples, or use branded products' sidebarTitle: 'Eco-mode vs products' icon: 'leaf' --- @@ -9,27 +9,47 @@ icon: 'leaf'
-**What it is.** There are two ways to hit an athlete's fueling targets. **Eco-mode** mixes a bottle from staples — table sugar, maltodextrin, a pinch of salt — for a fraction of the cost. **Products** uses branded gels, drink mixes, and chews, matched from Saturday's curated database. Both reach the same numbers; they trade off cost, precision, and convenience. +**What it is.** There are two ways to hit an athlete's fueling targets. The kitchen route mixes a bottle from staples: table sugar, maltodextrin, dextrose or fructose for carbohydrate, and salt, sodium citrate or sodium bicarbonate for sodium. The products route uses branded gels, drink mixes and chews matched from Saturday's curated database. -**When to turn it.** Eco-mode for budget-conscious athletes or anyone happy to mix their own. Products for athletes who want plug-and-play convenience or a specific tuned blend. A mix of both is completely fine — eco-mode the long bottles, a gel for the surges. +Both routes hit the same prescription. The dial decides which sources fill the gap; it never moves the carbohydrate, sodium or fluid totals. What changes is cost per hour, how precisely the blend lands, and how much mixing the athlete does. -**What changes for the athlete.** Their cost per hour, how precisely they hit the blend, and how much hands-on mixing they do. +**When to change it.** Toward the kitchen for budget-conscious athletes, or anyone happy to mix their own. Toward products for athletes who want plug-and-play convenience or a specific tuned blend. -**The question it answers.** *"Do I need to buy fancy gels, or can I mix my own?"* +**The question it answers.** *"Do I need to buy gels, or can I mix my own?"* -You flip this in an athlete's Setup — open one from your **[roster](https://coach.saturday.fit/dashboard)** to switch them between eco-mode and products. +## It is a spectrum, not an either-or + +Underneath, this is not a binary choice. The athlete's app presents five positions running from kitchen to products: + +| Position | What it means | +|----------|---------------| +| Super Eco | Lean on the kitchen as far as it goes | +| Eco | Kitchen-first | +| Half & Half | Split between the two | +| Top-Up | Products first, kitchen fills what is left | +| Products Only | Branded products throughout | + +The two ends split further for athletes who want them: Ultra Eco past Super Eco, and Purist past Products Only. + +The setting can also be overridden per activity type, so an athlete can run kitchen mixes for long rides and products for race day without changing their default. + + + **No position is preselected.** An athlete who has never touched this runs Saturday's standard behavior, where the kitchen fills whatever the products do not cover. Picking a position is an opt-in override, so an unset dial is a real state, not a missing one. + + In the coach portal this currently appears as a single on-and-off control rather than the five-position spectrum. If an athlete has set a specific position in the app, adjust it with them rather than from the portal. + ## What you do as a coach -- **Toggle eco-mode** by opening the athlete from your **[roster](https://coach.saturday.fit/dashboard)** and tapping Adjust Setup, to switch them between mixing their own and using products. ([How the Setup works](/coaching/nutrition/setup-dials).) -- **Let Saturday surface products.** When products are the right call, Saturday matches specific options from its curated database for the athlete's targets and [ratio](/coaching/nutrition/gluc-fruc-ratio). +- **Read where the athlete sits** before changing anything, since the portal control and the app's spectrum do not line up one-to-one. +- **Let Saturday surface products.** When products are the right call, Saturday matches specific options against the athlete's targets and [ratio](/coaching/nutrition/gluc-fruc-ratio) from its curated database. - **Never improvise a brand.** Product recommendations come only from Saturday's product matching — it accounts for the athlete's targets, ratio, and what's actually available. Don't hand an athlete a brand name off the top of your head; let Saturday surface the matched options so the recommendation is grounded in their plan. + **Never improvise a brand.** Product recommendations come only from Saturday's product matching, which accounts for the athlete's targets, their ratio, and what is available. Do not hand an athlete a brand name from memory; let Saturday surface the matched options so the recommendation is grounded in their plan. ## See also -- [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) — the blend you're hitting either way. -- [Bottling — fill & mix](/coaching/nutrition/bottling-fill-mix) — how the fill gets delivered. -- [The Setup](/coaching/nutrition/setup-dials) — toggle eco-mode for an athlete. +- [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) for the blend you are hitting either way. +- [Bottling: fill & mix](/coaching/nutrition/bottling-fill-mix) for how the fill gets delivered. +- [The Setup](/coaching/nutrition/setup-dials) to change this for an athlete. diff --git a/coaching/nutrition/gluc-fruc-ratio.mdx b/coaching/nutrition/gluc-fruc-ratio.mdx index e9a1b17..a100578 100644 --- a/coaching/nutrition/gluc-fruc-ratio.mdx +++ b/coaching/nutrition/gluc-fruc-ratio.mdx @@ -1,6 +1,6 @@ --- title: 'Gluc:fruc ratio' -description: 'Why the carb blend — not just the carb amount — decides how much an athlete can absorb' +description: 'The balance of glucose to fructose in an athlete''s carbohydrate, and when to change it' sidebarTitle: 'Gluc:fruc ratio' icon: 'scale-balanced' --- @@ -9,24 +9,28 @@ icon: 'scale-balanced'
-**What it is.** The **gluc:fruc ratio** is the balance of glucose to fructose in an athlete's carb blend. The body absorbs these two sugars through different pathways, so a blend lets an athlete take in **more total carbohydrate per hour** than glucose alone — without the GI distress that comes from overloading one pathway. +**What it is.** The gluc:fruc ratio is the balance of glucose to fructose in an athlete's carbohydrate. The two sugars are absorbed across the gut by different transporters, so a blend draws on both pathways rather than saturating one. This is why blended carbohydrate is tolerated better at high intakes than the same grams of glucose alone. -**When to turn it.** When an athlete is pushing high carb targets (long or hard sessions), or when they report stomach trouble at intake levels that shouldn't be a problem. +**What the dial does in Saturday.** It sets the blend Saturday builds toward. Every carb source carries a known glucose fraction: table sugar is half glucose and half fructose, maltodextrin and dextrose are effectively all glucose, fructose is none. Saturday takes the carbohydrate-weighted average across everything in the recipe and composes the mix, or suggests a swap, to land on the ratio you asked for. -**What changes for the athlete.** A higher tolerated carb intake, and fewer "my stomach shut down" days. The ratio is how Saturday raises the ceiling on grams-per-hour safely. +**What it does not do.** The ratio does not set how many grams per hour the athlete is prescribed. That number comes from their profile: the carbohydrate intake they have reported handling, their satiety setting, and the session's demands. Changing the ratio changes what the carbohydrate is made of, not how much of it there is. If an athlete needs a higher carbohydrate target, that is a profile change, not a ratio change. -**The question it answers.** *"Why can't I just drink more of one sugar?"* +**When to change it.** When an athlete is pushing high carbohydrate targets on long or hard sessions, or when they report stomach trouble at intakes that have not previously bothered them. -You set the ratio in an athlete's Setup — open one from your **[roster](https://coach.saturday.fit/dashboard)** to see and change it. +**The question it answers.** *"Why can't I just drink more of one sugar?"* ## What you do as a coach -- **Read the athlete's current ratio** by opening them from your **[roster](https://coach.saturday.fit/dashboard)**. -- **Adjust within the allowed set** via Adjust Setup — Saturday offers the ratios that are physiologically sensible, with a default that works for most athletes. ([How the Setup works](/coaching/nutrition/setup-dials).) -- **Pair it with the mix.** The ratio and the fill (eco-mode vs products) work together — a branded blend may already carry a tuned ratio; an eco-mode mix lets you hit it from staples. See [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products). +- **Read the athlete's current ratio** by opening them from your [roster](https://coach.saturday.fit/dashboard). +- **Pick from the offered set.** The portal offers a fixed set of ratios, from pure glucose up to equal parts glucose and fructose. See [The Setup](/coaching/nutrition/setup-dials) for how to change one. +- **Pair it with the fill.** A branded product may already carry a tuned blend, while a kitchen mix lets you hit a ratio from staples. See [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products). + + + **An unknown blend is left out rather than guessed.** When a product's glucose-to-fructose split is not known, Saturday excludes it from the ratio and reports those grams separately, instead of assuming an even split. A ratio you see is computed from sources whose composition is known. + ## See also -- [Bottling — fill & mix](/coaching/nutrition/bottling-fill-mix) — where the blend gets delivered. -- [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) — how to hit the ratio you want. -- [The Setup](/coaching/nutrition/setup-dials) — adjust the ratio for an athlete. +- [Bottling: fill & mix](/coaching/nutrition/bottling-fill-mix) for where the blend gets delivered. +- [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) for how to hit the ratio you want. +- [The Setup](/coaching/nutrition/setup-dials) to adjust the ratio for an athlete. diff --git a/coaching/nutrition/overview.mdx b/coaching/nutrition/overview.mdx index cd29345..479e278 100644 --- a/coaching/nutrition/overview.mdx +++ b/coaching/nutrition/overview.mdx @@ -1,46 +1,46 @@ --- -title: 'The dials — how Saturday thinks about fueling' -description: 'Understand the four dials and you can answer your athletes'' nutrition questions for good' +title: 'The dials: how Saturday thinks about fueling' +description: 'The model behind an athlete''s fueling plan, and the dials you turn to change it' sidebarTitle: 'Nutrition overview' icon: 'sliders' --- # The dials -Learn how Saturday turns a fueling target into a real, drinkable plan, and you'll be able to answer your athletes' nutrition questions yourself. To see a real one, open an athlete from your **[roster](https://coach.saturday.fit/dashboard)**. - - - **The goal: empower your athletes.** Saturday does the sports-science math so you don't have to. This guide teaches you the model behind it — enough that fueling stops being a stream of one-off questions and becomes something you've already handled. - +Saturday does the sports-science math. This section covers the model underneath it, so you can read an athlete's plan, change it deliberately, and answer their questions yourself. To see a live one, open an athlete from your [roster](https://coach.saturday.fit/dashboard). ## How Saturday turns targets into bottles -Saturday calculates an athlete's per-hour targets — carbohydrate, sodium, fluid — from their profile and the session. Then it translates those targets into a **bottle-by-bottle, hour-by-hour plan** the athlete can actually execute. Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to see their plan, then Adjust Setup to shape it. Four dials shape that translation: +Saturday calculates an athlete's per-hour targets for carbohydrate, sodium and fluid from their profile and the session ahead. It then maps those targets onto the bottles and flasks the athlete carries, producing a bottle-by-bottle, hour-by-hour plan. + +Three dials shape that mapping, and they all live in one object called the Setup. - - How targets become real bottles (carriage → consumption). + + How targets become real bottles. - Why the carb blend matters as much as the carb amount. + Which sugars make up the carbohydrate. - Mix your own, or buy branded — and when each wins. + How far to lean on the kitchen versus branded products. - Where all four dials live, and how you adjust them. + Where all three live, and how you adjust them. -## The 30-second version +## The short version | Dial | In one line | |------|-------------| -| **Bottling — fill & mix** | How much goes in each bottle and how often the athlete drinks. | -| **Gluc:fruc ratio** | The sugar blend that lets an athlete absorb more carbs without GI trouble. | -| **Eco-mode vs products** | Mix from staples (cheap, flexible) or use branded products (precise, convenient). | -| **The Setup** | The one place every dial lives — read it, adjust it, it syncs to the athlete instantly. | +| Bottling: fill & mix | Which vessels the athlete carries, how big each is, and when they drink. | +| Gluc:fruc ratio | The balance of glucose to fructose in the carbohydrate. | +| Eco-mode vs products | Whether the kitchen or a branded product fills the gap. | +| The Setup | The one object all three live in. Read it, change it, and it syncs to the athlete. | + +A dial changes *how* a target is met. It does not change the target itself; the per-hour carbohydrate, sodium and fluid numbers come from the athlete's profile and the session. - See [Co-piloting, not locking](/coaching/overview) for how this works across the whole portal. + See [Co-piloting](/coaching/overview) for how coach and athlete share control across the portal. diff --git a/coaching/nutrition/setup-dials.mdx b/coaching/nutrition/setup-dials.mdx index d091bd1..7c796ec 100644 --- a/coaching/nutrition/setup-dials.mdx +++ b/coaching/nutrition/setup-dials.mdx @@ -1,6 +1,6 @@ --- title: 'The Setup' -description: 'Where all four dials live for an athlete — read it, adjust it, and it syncs instantly' +description: 'Where an athlete''s dials live, how you adjust them, and what the athlete sees' sidebarTitle: 'The Setup' icon: 'gear' --- @@ -9,38 +9,43 @@ icon: 'gear'
-**What it is.** The **Setup** is the single object where an athlete's four dials live together — their carriage and fill/mix, their [gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio), and their [eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) choice. It's the one place you go to read or change how an athlete fuels — open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to find it. +**What it is.** The Setup is the object holding an athlete's fueling dials together: their carriage and fill, their [gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio), and their [eco-mode position](/coaching/nutrition/eco-mode-vs-products). It is the one place to read or change how an athlete fuels. -**When to turn it.** Any time you onboard, review, or troubleshoot an athlete's fueling — it's your control panel for everything on the dial pages. Reach it from your **[roster](https://coach.saturday.fit/dashboard)**. +**When to open it.** Any time you onboard, review, or troubleshoot an athlete's fueling. -**What changes for the athlete.** Their whole bottle and consumption plan updates and **syncs instantly** to their app. The athlete is always notified and can always revert it on their own device. (See [Co-piloting, not locking](/coaching/overview).) +**What changes for the athlete.** Their bottle and consumption plan updates and syncs to their app immediately. The Setup lives on the same record the app watches in real time, so there is no separate publish step. **The question it answers.** *"Where do I actually go to change any of this?"* -## Adjust Setup, from athlete-detail +## Adjusting a Setup -Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)**, then **Adjust Setup**. A panel opens alongside the athlete's context: +Open an athlete from your [roster](https://coach.saturday.fit/dashboard), then **Adjust Setup**. A panel opens alongside the athlete's context. -1. **You see ground truth first.** The Setup opens showing the athlete's **current** dials read-only, with an Edit affordance — so you never make a blind change. -2. **You edit the dials.** Carriage count and concentration, the fill & mix mapping, the gluc:fruc ratio, and the eco-mode toggle — using the same allowed ranges the app enforces, so you can never set a value the app would reject. -3. **You see the impact in a sentence.** As you change a dial, Saturday shows a one-line preview ("This raises Sam's carb target to ~Xg/hr") — a sentence, not a graph. -4. **You save, and it syncs.** A confirming toast with **Undo**; the change reaches the athlete's app immediately. +1. **You see ground truth first.** The panel opens read-only on the athlete's current dials, with an edit affordance, so you never make a blind change. +2. **You edit the dials.** Carriage slots for the chosen activity type, the gluc:fruc ratio, and the eco-mode control. +3. **You see the impact in a sentence.** As you change a dial, Saturday recomputes and shows one line naming the athlete and their resulting carbohydrate, sodium and fluid per hour under the Setup you are contemplating. A sentence, not a graph. +4. **You save.** The change reaches the athlete's app immediately, and a confirming toast offers a short undo window. ## The trust model -- **The athlete is always told.** A coach Setup change fires a good-news notification to the athlete with a "see what changed / revert" option. -- **The athlete's revert is the ultimate undo.** They can always undo a coach change on their own device. You co-pilot; they keep the wheel. -- **You can undo too.** A short soft-undo window on save reverses to the pre-change snapshot before any athlete notification fires. -- **Every edit is recorded.** Coach Setup edits are written to the org audit log with before/after, so there's always a record of what changed. You make these edits from an athlete in your **[roster](https://coach.saturday.fit/dashboard)**. +- **The athlete is told.** A coach Setup change sends the athlete a notification naming you, linking to a screen showing that their Setup changed. +- **The athlete keeps control.** They can open their Setup and change anything you set. You co-pilot; they keep the wheel. +- **You get a brief undo.** For about seven seconds after saving, the toast offers Undo, which writes the previous values back. It re-reads first, so an undo cannot clobber a change the athlete made in the meantime. +- **Every edit is recorded.** Coach Setup edits are written to the org audit log with before and after values. + + + **Undo does not un-send the notification.** The athlete is notified as soon as you save, not after the undo window closes. If you save and then undo, the athlete may already have been pinged, and undoing sends a second notification. When you are unsure of a change, use the impact sentence to check it before saving rather than relying on the undo. + ## Edge cases worth knowing -- **No Setup yet?** Open the athlete from your **[roster](https://coach.saturday.fit/dashboard)** and pre-fill it from defaults to help a less-techy athlete get started — they'll see it when they open the app (assisted onboarding). -- **Athlete editing at the same time?** If the athlete changes their Setup while you're editing, Saturday shows you what they now have and lets you decide — it never blindly clobbers the athlete's own change. -- **No edit permission?** If you're an assistant without Setup-edit access, the Adjust Setup action is hidden — ask your head coach to grant it from your **[team](https://coach.saturday.fit/dashboard/team)** settings. See [Permissions](/coaching/team/permissions). +- **The athlete has no Setup yet.** Open them from your roster and pre-fill from defaults, so a less technical athlete arrives to a working plan. +- **The athlete edits at the same time.** If the athlete changes their Setup while you are editing, your save is rejected rather than applied. Saturday shows you what they now have and lets you keep yours, take theirs, or merge. It never silently overwrites the athlete's own change. +- **You lack Setup-edit access.** Adjusting a Setup is a separate grant an assistant coach does not hold by default. Without it, your save is refused and you are told to ask your head coach for access. See [Permissions](/coaching/team/permissions). +- **A malformed change is refused whole.** If a Setup write arrives in the wrong shape, Saturday rejects the entire write rather than applying part of it, so a bad request can never half-land on the athlete's record. ## See also -- [The dials overview](/coaching/nutrition/overview) — the model behind the Setup. -- [The AI report](/coaching/roster/ai-report) — apply an AI plan straight into the Setup. -- [Bottling — fill & mix](/coaching/nutrition/bottling-fill-mix) · [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) · [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) +- [The dials overview](/coaching/nutrition/overview) for the model behind the Setup. +- [The AI report](/coaching/roster/ai-report) to open a Setup straight from an athlete's report. +- [Bottling: fill & mix](/coaching/nutrition/bottling-fill-mix) · [Gluc:fruc ratio](/coaching/nutrition/gluc-fruc-ratio) · [Eco-mode vs products](/coaching/nutrition/eco-mode-vs-products) diff --git a/coaching/overview.mdx b/coaching/overview.mdx index e0dbbfa..fd7b2fb 100644 --- a/coaching/overview.mdx +++ b/coaching/overview.mdx @@ -1,6 +1,6 @@ --- title: 'Coaching on Saturday' -description: 'Everything you need to coach athletes on Saturday — billing, roster, alerts, your team, and the fueling itself' +description: 'The coach portal: billing, roster, alerts, your team, and the fueling itself' sidebarTitle: 'Overview' icon: 'whistle' --- @@ -9,19 +9,19 @@ import { AppAction } from "/snippets/app-action.jsx" # Coaching on Saturday -These docs are for **coaches** using the Saturday **[coach portal](https://coach.saturday.fit/dashboard)** — not developers. (Building an integration? That's the [API Guides](/introduction) tab.) +These docs are for **coaches** using the Saturday **[coach portal](https://coach.saturday.fit/dashboard)**, not developers. Integration work lives in the [API Guides](/introduction) tab. -Saturday handles the fueling so you can focus on the training. This tab teaches you the whole coaching surface — and, just as importantly, **how Saturday thinks about nutrition**, so you can answer your athletes' fueling questions once and for all. +Saturday does the fueling math so you can spend your time on the training. This tab covers the coaching surface end to end, and how Saturday arrives at a fueling number, so you can answer an athlete's questions without leaving the conversation. Two ways in, one place you land. - From empty roster to a coached athlete in minutes. + From an empty roster to a coached athlete. - The billing modes, in plain language. + The four billing modes and how they combine. How Saturday turns targets into bottles. @@ -33,17 +33,17 @@ Saturday handles the fueling so you can focus on the training. This tab teaches | Area | What it covers | |------|----------------| | [Getting started](/coaching/become-a-coach) | Become a coach, invite or create your first athlete from your **[roster](https://coach.saturday.fit/dashboard)** | -| [Billing](/coaching/billing/who-pays) | Who pays, coach-paid athletes, take-over & relinquish, the volume discount — all in **[billing](https://coach.saturday.fit/admin/billing)** | +| [Billing](/coaching/billing/who-pays) | Who pays, coach-paid athletes, take-over & relinquish, the volume discount. Coverage is decided per athlete on your **[roster](https://coach.saturday.fit/dashboard)**; your own plan lives on **[Coach Subscription](https://coach.saturday.fit/admin/subscription)** | | [Roster & alerts](/coaching/roster/flags-and-groups) | Flags, groups, the AI report, and configuring your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)** | -| [Team & assistants](/coaching/team/add-assistants) | Add assistants, assign athletes, the team roll-up, and permissions on your **[team](https://coach.saturday.fit/dashboard/team)** | -| [Nutrition guide](/coaching/nutrition/overview) | The four dials: bottling & fill/mix, gluc:fruc ratio, eco-mode vs products, the Setup — open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to adjust it | +| [Team & assistants](/coaching/team/add-assistants) | Add assistants (Pro Coach and above), assign athletes, and set permissions on **[Members](https://coach.saturday.fit/admin/members)**. The **[Team](https://coach.saturday.fit/dashboard/team)** roll-up is Head Coach and above | +| [Nutrition guide](/coaching/nutrition/overview) | The three dials, bottling fill & mix, gluc:fruc ratio, and eco-mode vs products, plus the Setup they all live in. Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to adjust it | -## Co-piloting, not locking +## Co-piloting -You are **co-piloting**, not locking. Everything you adjust for an athlete — their fueling **Setup** (open an athlete from your **[roster](https://coach.saturday.fit/dashboard)**), their **[alerts](https://coach.saturday.fit/dashboard/alert-settings)** — syncs to their app instantly, the athlete is always notified, and the athlete can always change it back. The relationship is high-trust by design: Saturday does the math so you and your athlete can both see the same picture. +Everything you adjust for an athlete, their fueling **Setup** (open an athlete from your **[roster](https://coach.saturday.fit/dashboard)**) and their **[alerts](https://coach.saturday.fit/dashboard/alert-settings)**, syncs to their app instantly. The athlete is notified, and the athlete can change it back. Saturday does the math so you and your athlete are working from the same picture. - **You're also an athlete.** Every coach has their own Saturday athlete account. You can flip between your **Coaching** view and your **My Training** view with the persona toggle in the app — same account, two lenses. + **You're also an athlete.** Every coach has their own Saturday athlete account. The persona toggle on the app's Accounts page switches between **My Training** and **Coaching** on the same account. The Coaching side opens the web portal, which is where the roster and coaching tools live. -Ready to start? Open your **[dashboard](https://coach.saturday.fit/dashboard)** and follow along. +Open your **[dashboard](https://coach.saturday.fit/dashboard)** to follow along. diff --git a/coaching/roster/ai-report.mdx b/coaching/roster/ai-report.mdx index 79db627..2daa38d 100644 --- a/coaching/roster/ai-report.mdx +++ b/coaching/roster/ai-report.mdx @@ -1,43 +1,45 @@ --- title: 'The AI report' -description: 'A plain-language read on each athlete''s fueling — grounded only in their real numbers' +description: 'A plain-language read on each athlete''s fueling, computed from their own session numbers' sidebarTitle: 'The AI report' icon: 'file-lines' --- # The AI report -Every athlete on your **[roster](https://coach.saturday.fit/dashboard)** has an **AI fueling report** — a short, plain-language narrative of how their fueling has been going, grounded only in their real numbers, plus the structured summary behind it. To read one, open an athlete from your **[roster](https://coach.saturday.fit/dashboard)**. It's the fastest way to understand an athlete before a conversation. +Every athlete on your [roster](https://coach.saturday.fit/dashboard) has an AI fueling report: a short narrative of how their fueling has been going, plus the structured summary it was built from. Open an athlete from your roster to read one. ## What's in it -- **A narrative** — a calm, third-person read ("Over the last two weeks, this athlete has hit carb targets consistently but run low on sodium on long rides…"). No hype, no scare tactics, just what the data says. -- **The structured summary** — the concern breakdown (carb / sodium / fluid adherence, symptoms, patterns) that the narrative is built from. -- **Window & focus controls** — look back 7, 14, or 30 days; focus on the worst sessions, a rolling view, or key sessions. +- **A narrative.** A calm, third-person read, for example "Over the last two weeks, this athlete has hit carb targets consistently but run low on sodium on long rides." It is written only from server-computed session numbers, never from free text. +- **The structured summary.** The concern breakdown behind the narrative: carb, sodium and fluid adherence, symptoms, and patterns. See [the concern cutoffs](/coaching/roster/flags-and-groups) for what counts as a concern. +- **Window and focus.** Look back 7, 14, or 30 days, and focus on the worst sessions, a rolling view, or key sessions. The default is 14 days on worst sessions. -The report is served from cache and regenerates when a newer session lands (or when you refresh it). Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to see all of this. +The report is served from cache and regenerates when a newer in-window session lands, or when you refresh it. If generation fails, you get a deterministic summary built from the same numbers rather than an error. + + + **Window and focus selection needs Pro Coach or higher.** On the free Coach tier the report runs at your resolved default window and focus, and refreshes once per athlete per 24 hours. You still get a report at any time; between refreshes it comes from cache. + ## Make it actionable -The report isn't just a read — it's a launch point: +From the report you can: -- **Apply this plan to their Setup** — bridge straight from the report to [Adjust Setup](/coaching/nutrition/setup-dials), pre-filled from the AI plan. Open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to do this; the report becomes a one-click adjustment. -- **Reach out** — start an email or **[message](https://coach.saturday.fit/dashboard/messages)** to the athlete from here. +- **Open the athlete's Setup.** "Apply this plan to their Setup" opens [Adjust Setup](/coaching/nutrition/setup-dials) alongside the report so you can act on what you just read. The dials open on the athlete's current values, not on values copied from the plan, so you decide what to change. +- **Reach out.** Start an email or a [message](https://coach.saturday.fit/dashboard/messages) to the athlete. ## The privacy line -An athlete's **derived** fueling plan and report are shared with you. Their **raw AI conversations are not** — that's the single thing Saturday keeps private from a coach. There's no way to read an athlete's chat transcript. Everything else about the athlete's fueling is visible to you by default when you open them from your **[roster](https://coach.saturday.fit/dashboard)**. +An athlete's derived fueling plan and report are shared with you. Their raw AI conversations are not. There is no way to read an athlete's chat transcript, and no permission exists that would grant it. Everything else about the athlete's fueling is visible to you by default. - - **Why the carve-out?** Athletes talk to Saturday's AI candidly. Keeping those raw conversations private is what keeps them candid — and you still get the full, useful result: the plan and the report. - +Athletes talk to Saturday's AI candidly, and keeping those conversations private is what keeps them candid. The derived plan and report carry the result. -## Athletes never see these metrics +## Coach-facing metrics -The flags on your **[roster](https://coach.saturday.fit/dashboard)**, the concern summaries, the roll-up — these are **coach-facing only**. Saturday deliberately does not show athletes trend graphs, scores, or gap metrics; athletes get a plain, supportive mirror of their own fueling. Your coaching view is a distinct surface — it never leaks to the athlete. +The flags on your roster, the concern summaries, and the [team roll-up](/coaching/team/team-rollup) are coach-facing only. Saturday does not show athletes trend graphs, scores, or gap metrics; athletes get a plain, supportive mirror of their own fueling. Your coaching view is a separate surface and never reaches the athlete. ## See also -- [Flags & groups](/coaching/roster/flags-and-groups) — where the report's flags surface. -- [The Setup](/coaching/nutrition/setup-dials) — turn the report into an adjustment. -- [Configuring alerts](/coaching/roster/configuring-alerts) — get told when a report would change. +- [Flags & groups](/coaching/roster/flags-and-groups) for where the report's flags surface. +- [The Setup](/coaching/nutrition/setup-dials) to turn the report into an adjustment. +- [Configuring alerts](/coaching/roster/configuring-alerts) to be told when a report would change. diff --git a/coaching/roster/configuring-alerts.mdx b/coaching/roster/configuring-alerts.mdx index be56900..2dd16ca 100644 --- a/coaching/roster/configuring-alerts.mdx +++ b/coaching/roster/configuring-alerts.mdx @@ -1,55 +1,63 @@ --- title: 'Configuring alerts' -description: 'Tell Saturday exactly when to ping you — by concern, threshold, urgency, and channel' +description: 'Tell Saturday when to ping you, by concern, threshold, urgency, and channel' sidebarTitle: 'Configuring alerts' icon: 'bell' --- # Configuring alerts -Alerts are how Saturday tells you an athlete needs attention without you having to check the roster constantly. From your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)**, you decide **what** to be alerted about, **how loud**, and **where** it lands. The goal: real-time for what's urgent, a digest for the rest. +Alerts tell you an athlete needs attention without you having to check the roster. From your [alert settings](https://coach.saturday.fit/dashboard/alert-settings) you decide what to be alerted about, how loud, and where it lands. + +## What happens if you change nothing + +A coach who never touches a dial gets in-portal notifications only. No email, no push. Under-fueling, symptoms, low session ratings and dialing-down trends are on in the in-portal lane from day one; the hyponatremia pattern, the sleep trend, and the gone-quiet flag start off and are opt-in. + + + **Editing alert rules needs Pro Coach or higher, and so do the paid channels.** On the free Coach tier, and after a paid tier lapses, you can still read your configuration and receive in-portal notifications, but email and push are not delivered. + ## Start with a preset -The fastest setup is a preset applied across your whole roster, which you then tweak. Pick one in your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)**: +The fastest setup is a preset applied across your whole roster, which you then refine. | Preset | Behavior | |--------|----------| -| **Hands off** | In-portal only, no pings | -| **Balanced** | Core concerns by email, bundled into a daily digest | -| **Hands on** | All triggers, real-time urgent push | +| **Hands off** | In-portal only, no email or push. The default. | +| **Balanced** | Core concerns on email in a daily digest. The hyponatremia pattern is turned on and routed in real time, since it is a safety signal. Push stays off. | +| **Hands on** | All seven triggers on, email and push on, urgent items in real time, and a slightly earlier under-fueling trip point. | ## Then refine by scope -Alerts resolve at three scopes, and the **most specific wins**: +Rules resolve at three scopes, layered from broadest to narrowest: -- **Overall** — your whole roster. -- **Group** — one group (e.g. your elite squad gets real-time, everyone else gets a digest). Create groups first in **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. -- **Athlete** — a single athlete. +- **Overall**, your whole roster. +- **Group**, one group. Create groups first in your [roster](https://coach.saturday.fit/dashboard). +- **Athlete**, a single athlete, which wins last. -So you might run "Balanced" overall, "Hands on" for your race-prep group, and a custom rule for one athlete you're watching closely — all from your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)**. +So you might run Balanced overall, Hands on for your race-prep group, and a custom rule for one athlete you are watching closely. -## What you can configure +When an athlete belongs to more than one group, Saturday does not pick a group arbitrarily. It reduces across every group the athlete matches and takes the most sensitive setting, then applies any athlete-level override on top. -- **Concern types (triggers):** under-fueling, symptoms, low session ratings, a hyponatremia pattern, a dialing-down trend, a sleep trend, or an athlete who's gone quiet. -- **Thresholds:** the line that counts as a concern (e.g. sodium under 60%), and a separate urgent line for real-time pings. -- **Urgent vs. digest:** real-time for the urgent stuff, a once-a-day bundle for the rest — one item per athlete. -- **Channels:** in-portal, email, mobile push, and **[webhook](https://coach.saturday.fit/admin/webhooks)** (for power users / Enterprise pushing into their own systems). SMS is not offered for now. -- **Quiet hours:** suppress non-urgent pings overnight. -- **Combinations:** "ping me only when sodium is short **and** there's a symptom in the same session" — bounded two-trigger rules so you're not drowned in noise. +## What you can configure -Build all of this in your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)**. +- **Triggers.** Seven of them: under-fueling, symptoms, low session ratings, a hyponatremia pattern, a dialing-down trend, a sleep trend, and an athlete who has gone quiet. +- **Thresholds.** The line that counts as a concern, which defaults to the [shared concern cutoff](/coaching/roster/flags-and-groups) of 70% of prescribed, and a separate urgent line for real-time pings, which defaults to 50% for under-fueling. Leave either unset and it inherits the shared cutoff. +- **Urgent versus digest.** Real time for urgent items, a once-a-day bundle for the rest, one bundle per athlete per day. +- **Channels.** In-portal, email, and mobile push. SMS is not offered. +- **Quiet hours.** Suppress non-urgent pings overnight, in your own time zone. +- **Combinations.** Two-trigger AND rules, for example "ping me only when sodium is short and there is a symptom in the same session". Both legs must be session-level triggers; the sleep trend and the gone-quiet flag describe a window rather than a single session, so they cannot be combined. - **It's a friendly rule-builder, not a config file.** You build alert rules in plain GUI controls. Power users can also drive the exact same config over the [Coach API](/guides/coach-api) or the [Claude connector](/guides/coach-connector) — they're two views of one model. + **Webhook delivery is not live yet.** You can select the webhook channel on a rule, and the portal marks it as pending rather than silently dropping it. Managing an organization's webhooks is a Business and Enterprise capability. For programmatic alerting today, use the [Coach API](/guides/coach-api) or the [Claude connector](/guides/coach-connector), which read and write the same rule model the GUI does. ## Payment-failure alerts -Turn these on in your **[alert settings](https://coach.saturday.fit/dashboard/alert-settings)**: you can be alerted when **any** roster athlete — coach-paid or self-paying — has a payment failure, so an athlete never quietly loses access on your watch. When the athlete is the payer, they're alerted too (at a noise level you set), and they're deep-linked to update their card. Who pays for each athlete is managed in **[billing](https://coach.saturday.fit/admin/billing)**; see [Take over & relinquish](/coaching/billing/take-over-relinquish) for the grace-window behavior behind this. +You can be alerted when any roster athlete, coach-paid or self-paying, has a payment failure, so an athlete never quietly loses access on your watch. This rule is configured alongside the fueling triggers and carries no percentage threshold. When the athlete is the payer they are alerted too, at a noise level you set, and deep-linked to update their card. Who pays for each athlete is set from the athlete's row menu on your [roster](https://coach.saturday.fit/dashboard), under Cover, Stop covering, and Change payer; see [Take over & relinquish](/coaching/billing/take-over-relinquish) for the grace-window behavior behind this. ## See also -- [Flags & groups](/coaching/roster/flags-and-groups) — the concern definition the alerts use. -- [The AI report](/coaching/roster/ai-report) — the narrative an alert points you to. -- [Coach API](/guides/coach-api) — configure alerts programmatically. +- [Flags & groups](/coaching/roster/flags-and-groups) for the concern definition the alerts build on. +- [The AI report](/coaching/roster/ai-report) for the narrative an alert points you to. +- [Coach API](/guides/coach-api) to configure alerts programmatically. diff --git a/coaching/roster/flags-and-groups.mdx b/coaching/roster/flags-and-groups.mdx index f4356e7..2accab7 100644 --- a/coaching/roster/flags-and-groups.mdx +++ b/coaching/roster/flags-and-groups.mdx @@ -7,43 +7,54 @@ icon: 'flag' # Flags & groups -Your **[roster](https://coach.saturday.fit/dashboard)** is the home base for coaching. It surfaces **who needs your attention** without making you read every athlete's data, and it lets you **group** athletes so you can act on many at once. +Your [roster](https://coach.saturday.fit/dashboard) surfaces who needs your attention without making you read every athlete's data, and it lets you group athletes so you can act on many at once. -## Flags — who needs attention +## Flags -Each athlete on your **[roster](https://coach.saturday.fit/dashboard)** carries a needs-attention summary over a look-back window (7, 14, or 30 days). An athlete who's been fueling well shows no flag and is never alarmed; an athlete who crossed a concern bar is flagged with the top reasons ("Sodium 57%", "1 symptom"). +Each athlete carries a needs-attention summary over a look-back window of 7, 14, or 30 days, defaulting to 14. An athlete who has been fueling well shows no flag. An athlete who crossed a concern cutoff is flagged with the top reasons, such as "Sodium 57%" or "1 symptom". -- **Sortable Last-Activity column** so you instantly see who's gone quiet. -- **Payer-status column** so you see who pays for each athlete at a glance — see [Who pays](/coaching/billing/who-pays). -- **Filters** to narrow by flag, group, payer status, or assigned assistant. -- **Inline actions** — cover/uncover, add to group, message — without leaving the roster. +A missing value is never a flag. An unrated session, an activity with no fueling reported, or a nutrient with no data stays silent rather than counting against the athlete. Only a value that is present and past its cutoff ever flags. -Flags come from Saturday's **one shared concern definition**, so what you see on the roster matches the digest, the AI report, and the alerts exactly. No two surfaces disagree. +The roster also gives you: + +- A last-activity column, sorted on the underlying date rather than the displayed text, so you can see who has gone quiet. +- A payer-status column showing who funds each athlete's membership. See [Who pays](/coaching/billing/who-pays). +- Filters for flag, group, payer status, and assigned assistant. +- Inline actions (cover and uncover, add to group, message) without leaving the roster. + +### The concern cutoffs + +Flags come from one shared concern definition, so the roster markers, the session table, the digest, and the worst-sessions focus in the AI report all read a session the same way. Four of the seven alert triggers are built on the same definition. The defaults: + +| Concern | Default cutoff | +|---------|----------------| +| Carbohydrate, sodium, or fluid | Below 70% of prescribed | +| Hyponatremia pattern | Fluid at or above 90% with sodium at or below 50% | +| Fueling symptom | Any fueling symptom at severity 2 or higher, on a 0 to 3 scale | +| Low session rating | A rating of 2 or lower, on a 1 to 5 scale | + +You can tune these cutoffs. An alert rule can also carry its own threshold at a given scope, which applies to that alert without moving the roster marker. See [Configuring alerts](/coaching/roster/configuring-alerts). ## Groups -Group athletes however you coach — by squad, by event, by training block ("70.3 Build"). Build and edit them from **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. Groups let you: +Group athletes however you coach: by squad, by event, by training block such as "70.3 Build". Build and edit groups from the group manager on your [roster](https://coach.saturday.fit/dashboard). A group lets you act on many athletes in one move, message everyone at once, hand the whole group to an assistant coach, and scope alert rules to that group. -- **Act in bulk** — add to a group, message a whole group, assign a group to an assistant. -- **Scope your alerts** — set different alert rules for different groups (see [Configuring alerts](/coaching/roster/configuring-alerts)). -- **Broadcast** — send a message to an entire group at once. +Org admins on Head Coach and above get a second, governance-oriented view at [Control panel, Groups](https://coach.saturday.fit/admin/groups): the whole org group tree, assigning an org group to an assistant, promoting a personal group so the org can share it, and a read-only view of who sees what. Day-to-day grouping still happens on the roster. ### Working with groups -All of the actions below live in **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. - | Action | What it does | |--------|--------------| -| Create a group | Name it; add athletes now or later | -| Add to group (bulk) | Multi-select athletes → **Add to group** | +| Create a group | Name it, then add athletes now or later | +| Add to group (bulk) | Multi-select athletes, then **Add to group** | | Assign group to assistant | Delegate a whole group to an assistant coach | | Group broadcast | Message everyone in the group | -| Delete group | Removes the grouping (not the athletes); undoable | +| Delete group | Removes the grouping, not the athletes | -Every consequential action — removing an athlete, deleting a group — confirms first and offers an undo. +Deleting a group and removing an athlete both ask for confirmation and then offer an undo before the change is committed. ## See also -- [The AI report](/coaching/roster/ai-report) — the narrative behind an athlete's flags. -- [Configuring alerts](/coaching/roster/configuring-alerts) — turn flags into the right pings. -- [Team & assistants](/coaching/team/assign-athletes) — delegate groups to your team. +- [The AI report](/coaching/roster/ai-report) for the narrative behind an athlete's flags. +- [Configuring alerts](/coaching/roster/configuring-alerts) to turn flags into the right pings. +- [Assign athletes](/coaching/team/assign-athletes) to delegate groups to your team. diff --git a/coaching/team/add-assistants.mdx b/coaching/team/add-assistants.mdx index 96891d0..5cb7166 100644 --- a/coaching/team/add-assistants.mdx +++ b/coaching/team/add-assistants.mdx @@ -7,50 +7,52 @@ icon: 'user-group' # Add assistants -On **Head Coach** and above, you can bring **assistant coaches** onto your team to share the roster load from your **[team page](https://coach.saturday.fit/dashboard/team)**. Each assistant gets their own login and a scoped view of the athletes you assign them. +From Pro Coach up, you can bring assistant coaches onto your team to share the roster load. Each assistant gets their own login, their own book of athletes, and whatever powers you delegate to them. You manage the team itself from your [team page](https://coach.saturday.fit/dashboard/team). -## Who can add assistants +## Assistants included with each plan -| Plan | Included assistants | +| Plan | Assistants included | |------|---------------------| -| Pro Coach | +1 | -| Head Coach | +5 | -| Business | Unlimited (fair-use) | -| Enterprise | Custom | +| Coach (free) | None | +| Pro Coach | 1 | +| Head Coach | 5 | +| Business | Unlimited | +| Enterprise | Unlimited | -Assistant seats are capped by your plan (unlike athlete seats, which flex). If you need more, **[upgrade your plan](https://coach.saturday.fit/admin/billing)**. +Reaching the included count is not a wall. On Pro Coach and Head Coach you can buy additional assistant seats at $10 per seat per month, and they stack on top of the included allotment, so a Pro Coach with three purchased seats can hold four assistants without changing plan. The portal offers the seat inline when you hit the cap. Business and Enterprise have no assistant cap, so the seat add-on does not apply to them. + + + **A separate limit governs Business organizations.** A Business org's total coach seats are capped by a fair-use ceiling of 20 coaches, or one coach per five coach-paid athletes, whichever is higher. That ceiling counts coaches across the org, not assistants under one head coach. + ## Invite an assistant -From your **[team page](https://coach.saturday.fit/dashboard/team)** (or **Settings → Team**), click **Invite assistant** and enter their email. They get an invite to join your team. When they accept and activate, they appear on your team and can be assigned athletes. +From your [team page](https://coach.saturday.fit/dashboard/team), or **Settings → Team**, choose **Invite assistant** and enter their email. When they accept and activate, they appear on your team and can be assigned athletes. - - **An assistant sees only what you give them.** A freshly added assistant starts with the visibility you grant — assign them athletes or whole groups to fill their view. See [Assign athletes](/coaching/team/assign-athletes). - +Once an assistant activates, they can read your whole roster, and assigning athletes to them records which ones they own. See [Assign athletes](/coaching/team/assign-athletes) for what assignment does and does not control. ## What an assistant can do -By default, an assistant can coach the athletes assigned to them — view fueling data, read AI reports, adjust Setups (if granted), and message athletes. What they **can't** do by default: +By default an assistant can coach the athletes assigned to them: view fueling data, read AI reports, and message the athlete. What an assistant cannot do by default: -- End an **org-paid** coverage arrangement (unless you explicitly grant it) — those money actions live in **[billing](https://coach.saturday.fit/admin/billing)**. +- Adjust an athlete's Setup. That is its own grant, separate from general athlete access, so an assistant can read and act on an athlete without being able to re-dial their fueling. +- End an org-paid coverage arrangement. That is a money action reserved for the head coach or org owner, and it has its own toggle. - Manage billing or the team itself. -- See athletes they haven't been assigned. - -You control all of this with roles — see [Permissions](/coaching/team/permissions). ## Manage your team | Action | What it does | |--------|--------------| | Resend invite | Re-send a pending assistant invite | -| Remove assistant | Take an assistant off the team (undoable) | -| Assign athletes / groups | Give an assistant a roster to coach | -| Set role | Choose a preset role or a custom one | +| Remove assistant | Take an assistant off the team | +| Assign athletes or groups | Give an assistant a roster to coach | + +Removing an assistant asks for confirmation and then offers an undo. -Removing an assistant is a consequential action — it confirms first and offers an undo. Do all of this from your **[team page](https://coach.saturday.fit/dashboard/team)**. +Roles and per-assistant grants are set separately, on the [Access & Roles](https://coach.saturday.fit/admin/rbac) page. See [Permissions](/coaching/team/permissions). ## See also -- [Assign athletes](/coaching/team/assign-athletes) — fill an assistant's roster. -- [The team roll-up](/coaching/team/team-rollup) — see how your whole team is doing. -- [Permissions](/coaching/team/permissions) — exactly what each role can do. +- [Assign athletes](/coaching/team/assign-athletes) to fill an assistant's roster. +- [The team roll-up](/coaching/team/team-rollup) to see how your whole team is doing. +- [Permissions](/coaching/team/permissions) for exactly what each role can do. diff --git a/coaching/team/assign-athletes.mdx b/coaching/team/assign-athletes.mdx index f3958b3..a7c29f5 100644 --- a/coaching/team/assign-athletes.mdx +++ b/coaching/team/assign-athletes.mdx @@ -1,41 +1,48 @@ --- title: 'Assign athletes' -description: 'Give each assistant the right athletes — one at a time, in bulk, or by group' +description: 'Give each assistant the right athletes, one at a time, in bulk, or by group' sidebarTitle: 'Assign athletes' icon: 'arrows-split-up-and-left' --- # Assign athletes -Assistants coach the athletes you assign them. Assigning is how you split a big **[roster](https://coach.saturday.fit/dashboard)** across your team so each assistant has a clear, scoped book of athletes. +Assigning is how you split a big [roster](https://coach.saturday.fit/dashboard) across your team, so each assistant coach has a clear book of athletes they own. ## Three ways to assign -- **One athlete:** on athlete-detail or your **[roster](https://coach.saturday.fit/dashboard)**, set the **assigned assistant**. -- **In bulk:** multi-select athletes on your **[roster](https://coach.saturday.fit/dashboard)** → **Assign to assistant** → pick the assistant. -- **By group:** assign an entire [group](/coaching/roster/flags-and-groups) to an assistant in one move from **[athlete management](https://coach.saturday.fit/dashboard?view=manage)** — great for delegating a whole squad. +- **One athlete.** On athlete-detail or your [roster](https://coach.saturday.fit/dashboard), set the assigned assistant. +- **In bulk.** Multi-select athletes on your roster, choose **Assign to assistant**, and pick the assistant. +- **By group.** Assign an entire [group](/coaching/roster/flags-and-groups) to an assistant in one move from your [roster](https://coach.saturday.fit/dashboard). - - **Assigning is what gives access.** When you assign an athlete to an assistant, that link is what gives the assistant access to that athlete. There's no separate step — assigning is the whole action. - +## What assignment does, and what it does not do + +Assignment sets **ownership**: which assistant is responsible for that athlete. It drives the assigned-assistant chip on the roster, the per-coach load figures in the [team roll-up](/coaching/team/team-rollup), and which athletes land in that assistant's own book. + +Assignment is not what first grants an assistant read access. + + + **An activated assistant can already see your whole roster, read-only, before you assign them anything.** When an assistant accepts their invite and activates, Saturday adds them to every active athlete relationship you hold, so they can open any athlete on your roster and read that athlete's fueling data. Assigning athletes narrows nothing on its own; it records who owns whom. Unassigning an athlete does remove that assistant's access to that athlete's detail. + + If you need an assistant who genuinely cannot see the rest of your roster, that is a role and permission question, not an assignment question. See [Permissions](/coaching/team/permissions). + ## What an assigned assistant sees -Once assigned, the assistant sees that athlete on their roster with the full coaching surface they're permitted to use — fueling history, flags, AI report, and (if granted) [Adjust Setup](/coaching/nutrition/setup-dials). They can message the athlete and act on their assigned book just like you do on the **[full roster](https://coach.saturday.fit/dashboard)** — open an athlete from your roster to see the same surface. +An assistant working an athlete they own gets the coaching surface their role permits: fueling history, flags, the AI report, messaging, and [Adjust Setup](/coaching/nutrition/setup-dials) if you have granted Setup-edit access. Adjusting an athlete's Setup is a separate grant that an assistant does not hold by default. ## Reassigning and unassigning -- **Reassign** an athlete to a different assistant any time. -- **Unassign** to pull an athlete back to yourself (or leave them unassigned). +Reassign an athlete to a different assistant at any time, or unassign to pull them back to yourself. Reassignment takes effect immediately and changes nothing about the athlete: their data, history, and Setup are untouched, only the owning coach changes. -Reassignment is immediate and the athlete's coaching continuity is preserved — their data, history, and Setup don't change, only who's watching them. Do this from **[athlete management](https://coach.saturday.fit/dashboard?view=manage)**. +Do this from your [roster](https://coach.saturday.fit/dashboard). -## Head coach always sees everything +## Head coach visibility -You, as head coach (or org owner), retain visibility of the **whole** **[roster](https://coach.saturday.fit/dashboard)** regardless of assignment. Assigning to an assistant adds their access; it never removes yours. +As head coach or org owner you keep visibility of the whole roster regardless of assignment. Assigning to an assistant adds their ownership; it never removes yours. ## See also -- [Add assistants](/coaching/team/add-assistants) — get assistants onto your team first. -- [The team roll-up](/coaching/team/team-rollup) — per-coach load and performance. -- [Permissions](/coaching/team/permissions) — scope what an assistant can do with their assigned athletes. +- [Add assistants](/coaching/team/add-assistants) to get assistants onto your team first. +- [The team roll-up](/coaching/team/team-rollup) for per-coach load and performance. +- [Permissions](/coaching/team/permissions) to scope what an assistant can do. diff --git a/coaching/team/permissions.mdx b/coaching/team/permissions.mdx index b0411f3..b9d5398 100644 --- a/coaching/team/permissions.mdx +++ b/coaching/team/permissions.mdx @@ -1,52 +1,84 @@ --- title: 'Permissions' -description: 'Roles and access control — preset roles for everyone, custom roles for Business & Enterprise' +description: 'Roles, scopes, and access control across your team and organization' sidebarTitle: 'Permissions' icon: 'lock' --- # Permissions -Permissions decide what each member of your **[team](https://coach.saturday.fit/dashboard/team)** can do. The system is tiered: every coach gets **preset roles** for quick setup, and **Business / Enterprise** add a full **custom-role builder**. It's all GUI-first — no config files, no API required. +Permissions decide what each member of your [team](https://coach.saturday.fit/dashboard/team) can do. Roles, delegation, and custom roles are all managed on the [Access & Roles](https://coach.saturday.fit/admin/rbac) page, in three layers: preset roles for every coach, per-assistant delegation for every coach, and a custom-role builder on Head Coach and above. -## Preset roles (every coach) +## Roles and scopes -Assign a preset role to any assistant in a couple of clicks from your **[team page](https://coach.saturday.fit/dashboard/team)**: +A permission is never granted in the abstract. Every grant carries a **scope** that decides how much data it covers: -- **Assistant** — coach assigned athletes (view data, read reports, message); adjust Setup if granted. -- **Senior assistant** — broader access across the roster, as you choose. -- **Read-only** — view assigned athletes without making changes. +| Scope | Covers | +|-------|--------| +| Relationship | Only the athletes this coach has a direct coaching relationship with | +| Org | Every athlete in the organization | +| Saturday global | Reserved for Saturday staff | -## The assistant-billing-delegation toggle (every coach) +This is why a head coach and an assistant can both hold "view athlete profile" and see very different rosters: the head coach holds it at org scope, the assistant at relationship scope. -By default, an assistant **cannot end an org-paid coverage arrangement** — that's a money action reserved for the head coach or org owner. A single toggle on your **[team page](https://coach.saturday.fit/dashboard/team)** lets you **grant** an assistant that ability when you trust them with it. This toggle is available to every coach, not just the big tiers. +## Preset roles -## Custom roles (Business & Enterprise) +The built-in roles you can assign on [Access & Roles](https://coach.saturday.fit/admin/rbac): -Business and Enterprise get the **custom-role builder** on your **[team page](https://coach.saturday.fit/dashboard/team)**: name a role and check exactly which permissions it has. Build a role that fits how your organization actually works, then assign it to as many team members as you like. +| Role | Scope | What it is for | +|------|-------|----------------| +| Owner | Org | Full administration of one organization | +| Program Admin | Org | Org management without athlete data | +| Performance Director | Org | Org-wide read-only athlete data | +| Billing Manager | Org | Billing and invoicing for one organization | +| Head Coach | Org | Manage coaches and athletes in one organization | +| Coach | Relationship | Manage their own athletes | +| Assistant Coach | Relationship | Limited coaching, per the grants their head coach gives them | +| Read-Only Coach | Relationship | View selected athletes without making changes | + +Only an existing org owner, or Saturday staff, can assign the Owner role. A Program Admin cannot promote anyone to Owner, including themselves. + +## How a permission check resolves + +For any given action, Saturday resolves in this order and stops at the first answer: + +1. A per-user **deny** on that permission. A deny always wins, whatever the role says. +2. A per-user **grant**, at the scope it was granted. +3. The union of the member's role grants, taking the widest scope any of them provides. +4. A custom role definition, for roles that are not built in. +5. Otherwise, denied. + +## Per-assistant delegation, on every tier + +Three delegation axes let a head coach hand a specific power to a specific assistant without building a custom role. All three are available on every tier, and they compose: turning one on never disturbs the others. + +- **Billing.** Lets an assistant cover and uncover their assigned athletes' memberships. A **second, separate** toggle governs ending an org-paid coverage arrangement. Turning on billing delegation alone never grants the org-paid one. +- **Setup.** Lets an assistant adjust an athlete's Setup: carriage, fill and mix, gluc:fruc ratio, and eco-mode. Held by a head coach by default, never by an assistant unless granted. +- **Governance.** Grants org powers one at a time: branding, SAML configuration, audit-log reading, API-key management, webhook management, org-hierarchy management, and management of the custom roles themselves. + +## Custom roles, on Head Coach and above + +Head Coach, Business, and Enterprise get the custom-role builder on [Access & Roles](https://coach.saturday.fit/admin/rbac): name a role, then check exactly which permissions it carries and at what scope. Assign it to as many team members as you like. The same tier unlocks per-user permission overrides and the effective-access viewer, which shows what a given member can do once roles, grants, and denies have all been applied. | Capability area | Example permissions | |-----------------|---------------------| -| Roster | View assigned / view all, add to group | -| Athlete | Adjust Setup, message, add notes | -| Billing | Cover/uncover, end org coverage, issue refunds | -| Team | Invite assistants, assign athletes, manage roles | -| Org | Manage sub-orgs, view audit logs | +| Athlete data | View profile and activities, edit profile, adjust Setup, read AI summaries | +| Roster | Read at relationship or org scope, start a transfer | +| Billing | Preview coverage, cover and uncover, end org coverage, refund a coaching charge | +| Team | Read and write org members, assign roles | +| Org | Branding, API keys, webhooks, SAML, hierarchy, audit-log read | ## What no role can ever grant -One permission does not exist, on purpose: **viewing an athlete's raw AI conversations.** It isn't an assignable permission — even in the custom-role builder on your **[team page](https://coach.saturday.fit/dashboard/team)**, a role literally cannot turn it on. Athletes' raw AI chats stay private; their derived plans and reports are shared. See [The AI report](/coaching/roster/ai-report). - -## Audit logs - -Org owners (Business and above) can review an **[audit log](https://coach.saturday.fit/admin/audit)** of consequential actions across the org — who covered whom, who changed a Setup, who issued a refund. It's scoped to the org owner so you have accountability without exposing day-to-day coaching to everyone. +One permission does not exist, deliberately: viewing an athlete's raw AI conversations. It is absent from the permission catalog, so there is nothing for the custom-role builder to check. Athletes' raw AI chats stay private; their derived plans and reports are shared with you. See [The AI report](/coaching/roster/ai-report). -## Organizations & sub-orgs +## Organizations, audit, and hierarchy -Business and Enterprise support a full **[org hierarchy](https://coach.saturday.fit/admin/org)** — create sub-orgs, move coaches and athletes between them, and scope admins per sub-org. Permissions and coverage respect that hierarchy. +- **Org hierarchy** is available on Head Coach and above: create sub-organizations, move coaches and athletes between them, and scope admins per sub-org. Permissions and coverage respect the hierarchy. +- **The [audit log](https://coach.saturday.fit/admin/audit)** is a Business and Enterprise capability, scoped to the org owner. It records consequential actions across the org: who covered whom, who changed a Setup and what changed, who issued a refund, who edited alert rules. ## See also -- [Add assistants](/coaching/team/add-assistants) — bring people onto the team first. -- [Assign athletes](/coaching/team/assign-athletes) — scope the roster a role applies to. -- [Take over & relinquish](/coaching/billing/take-over-relinquish) — the org-coverage actions a role can gate. +- [Add assistants](/coaching/team/add-assistants) to bring people onto the team first. +- [Assign athletes](/coaching/team/assign-athletes) for what assignment does and does not control. +- [Take over & relinquish](/coaching/billing/take-over-relinquish) for the org-coverage actions a role can gate. diff --git a/coaching/team/team-rollup.mdx b/coaching/team/team-rollup.mdx index 14d94d8..52b8eb8 100644 --- a/coaching/team/team-rollup.mdx +++ b/coaching/team/team-rollup.mdx @@ -1,39 +1,45 @@ --- title: 'The team roll-up' -description: 'See how your whole operation is doing — per athlete, per coach, and in aggregate' +description: 'See how your whole operation is doing, per athlete, per coach, and in aggregate' sidebarTitle: 'Team roll-up' icon: 'chart-line' --- # The team roll-up -The **[team roll-up](https://coach.saturday.fit/dashboard/team)** is your bird's-eye view across the whole operation — engagement, coach load, aggregate trends, and which athletes might be drifting. It's a Head Coach-tier capability: where Pro Coach gives you per-athlete coaching, Head Coach and above add the management layer on top. +The [team roll-up](https://coach.saturday.fit/dashboard/team) is the view across your whole operation: engagement, coach load, aggregate trends, and which athletes might be drifting. It is available on Head Coach and above. Pro Coach gives you per-athlete coaching; Head Coach adds the management layer over a team. - - **Available on Head Coach and above.** On Pro Coach you coach athletes; on Head Coach you also run a team. - +Every view runs over a 7, 14, or 30 day window that you pick. ## The four views | View | What it answers | |------|-----------------| -| **Per-athlete engagement** | Who's active, who's gone quiet — last-active, activity count, adherence, and trend per athlete | -| **Per-coach load & performance** | How your assistants are doing — roster size, attention given, athlete outcomes | -| **Team aggregate trends** | How the whole roster is trending in aggregate | -| **At-risk / downgrade candidates** | Athletes who are underutilizing Saturday and may not be worth covering | - -All four live on your **[team roll-up](https://coach.saturday.fit/dashboard/team)**. +| **Per-athlete engagement** | Who is active and who has gone quiet: last active, activity count, average adherence, adherence trend, and flag count per athlete | +| **Per-coach load** | How your assistants are carrying the roster: athletes held, how many are active, average adherence, and flag count | +| **Team aggregate trends** | Total and active athletes, average adherence, flagged rate, and which way adherence is moving | +| **At-risk candidates** | Athletes who are underusing Saturday, with a score and the reasons behind it | ## At-risk candidates -The roll-up flags athletes who look like they're drifting, using a **composite signal** — usage, adherence, and recency together (not any single number). **You set the threshold** for what counts as at-risk, so it matches how you coach. From there you can reach out, open an athlete from your **[roster](https://coach.saturday.fit/dashboard)** to adjust their [Setup](/coaching/nutrition/setup-dials), or — for a [coach-paid athlete](/coaching/billing/coach-paid-athletes) — decide in **[billing](https://coach.saturday.fit/admin/billing)** whether to keep covering them. +The roll-up scores athletes on a composite of three signals: usage, adherence, and recency. No single number decides it. + +You control the scoring. Three presets set the weights and the cutoff for you: + +- **Conservative** flags fewer athletes and leans on recency, so it mostly surfaces people who have gone quiet. +- **Balanced** weights the three signals roughly evenly. This is the default. +- **Aggressive** flags more athletes and leans on usage and adherence drops. + +Adjust any weight or the threshold directly and the preset switches to custom. The candidate count updates as you move the slider, so you can see what a threshold change would surface before you commit to it. You can also dismiss a candidate to snooze them. + +From a candidate you can reach out, open the athlete to adjust their [Setup](/coaching/nutrition/setup-dials), or, for a [coach-paid athlete](/coaching/billing/coach-paid-athletes), use the athlete's row menu on your [roster](https://coach.saturday.fit/dashboard) to stop covering them or change their payer. -## A coach-only surface +## Coach-facing only -Like the [AI report](/coaching/roster/ai-report) and roster flags, your **[team roll-up](https://coach.saturday.fit/dashboard/team)** is **coach-facing only**. Athletes never see these metrics — no trend graphs, scores, or gap numbers reach the athlete. Saturday's athlete experience is a plain, supportive mirror; the coaching analytics are a separate management surface that stays on your side of the line. +Like the [AI report](/coaching/roster/ai-report) and roster flags, the roll-up is a coach-facing surface. Athletes never see these numbers. ## See also -- [Assign athletes](/coaching/team/assign-athletes) — the assignments the per-coach view rolls up. -- [The AI report](/coaching/roster/ai-report) — per-athlete depth behind the aggregate. -- [Flags & groups](/coaching/roster/flags-and-groups) — the concern definition the roll-up builds on. +- [Assign athletes](/coaching/team/assign-athletes) for the ownership the per-coach view rolls up. +- [The AI report](/coaching/roster/ai-report) for per-athlete depth behind the aggregate. +- [Flags & groups](/coaching/roster/flags-and-groups) for the concern definition the roll-up builds on. diff --git a/docs.json b/docs.json index 71195f9..c33e7b7 100644 --- a/docs.json +++ b/docs.json @@ -94,7 +94,6 @@ { "group": "Integration Guides", "pages": [ - "guides/ai-coach", "guides/webhooks", "guides/oauth2", "guides/organizations", diff --git a/drafts/ai-coach.draft.mdx b/drafts/ai-coach.draft.mdx new file mode 100644 index 0000000..d4800ff --- /dev/null +++ b/drafts/ai-coach.draft.mdx @@ -0,0 +1,266 @@ +--- +title: 'AI Coach Integration' +description: 'Embed Saturday AI coaching with SSE streaming in your platform' +sidebarTitle: 'AI Coach' +icon: 'robot' +--- + +# AI Coach Integration + +Saturday's AI Coach is conversational nutrition coaching, powered by Claude and Gemini, that partners can embed in their own platforms so athletes get nutrition advice without leaving your app. + + + The AI Coach endpoints are in **STEALTH** stage and require explicit alpha access. Until your partner account is enabled for them, every AI Coach path returns `404` with the same body as any unknown route, before authentication, so a `404` here means "not enabled for you", not "wrong URL". Contact [api@saturday.fit](mailto:api@saturday.fit) to request access. + + +The examples read your key from a `SATURDAY_API_KEY` environment variable, as in [Quickstart](/quickstart). Sandbox keys are issued with their own base URL; using one against `api.saturday.fit` returns `401 invalid_api_key`. + +## How it works + +The AI Coach answers against each athlete's profile, activity history, fueling preferences, and past feedback. It can: + +- Answer nutrition questions specific to the athlete's situation +- Explain why a prescription was calculated the way it was +- Help plan race-day nutrition strategies +- Recommend products based on preferences and tolerances +- Provide pre-activity preparation instructions + +Responses stream over Server-Sent Events (SSE). + +## Conversation lifecycle + +Create a conversation, which streams its first response; send further messages, each streaming its own response; read history; optionally delete. + +### Creating a conversation + +Creating a conversation sends the athlete's first message in the same call. Both `athlete_id` and `message` are required, and the response is an SSE stream, not a JSON body. The conversation ID arrives in the first event. + + + +```python Python +import json +import os +import requests + +response = requests.post( + "https://api.saturday.fit/v1/ai/conversations", + headers={ + "Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}", + "Content-Type": "application/json", + "Accept": "text/event-stream", + }, + json={ + "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "message": "What should I eat before my marathon on Saturday?", + }, + stream=True, +) + +conv_id = None +event = None +for line in response.iter_lines(): + line = line.decode("utf-8") if line else "" + if line.startswith("event: "): + event = line[7:] + elif line.startswith("data: "): + data = json.loads(line[6:]) + if event == "message_start": + conv_id = data["conversation_id"] + elif event == "text_delta": + print(data["delta"], end="", flush=True) +``` + +```typescript TypeScript +const response = await fetch( + "https://api.saturday.fit/v1/ai/conversations", + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ + athlete_id: "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + message: "What should I eat before my marathon on Saturday?", + }), + } +); + +let convId: string | undefined; +for await (const { event, data } of readSSE(response)) { + if (event === "message_start") convId = data.conversation_id; + else if (event === "text_delta") process.stdout.write(data.delta); +} +``` + + + +### Sending a message (SSE streaming) + +Subsequent messages go to the conversation's own path. The response streams exactly as above. + + + +```python Python +import os + +response = requests.post( + f"https://api.saturday.fit/v1/ai/conversations/{conv_id}/messages", + headers={ + "Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}", + "Content-Type": "application/json", + "Accept": "text/event-stream", + }, + json={"message": "Can I swap the gel for real food?"}, + stream=True, +) +``` + +```typescript TypeScript +const response = await fetch( + `https://api.saturday.fit/v1/ai/conversations/${convId}/messages`, + { + method: "POST", + headers: { + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, + "Content-Type": "application/json", + Accept: "text/event-stream", + }, + body: JSON.stringify({ message: "Can I swap the gel for real food?" }), + } +); +``` + + + +### SSE event format + +Every event has a named type and a JSON `data` payload. There is no `[DONE]` sentinel; the stream ends with `message_end`. + +``` +event: message_start +data: {"conversation_id": "9f3c1a7e-2b48-4d05-a6c1-7e0b3d5f8a12"} + +event: text_delta +data: {"delta": "The recommended"} + +event: text_delta +data: {"delta": " carbohydrate intake"} + +event: tool_call +data: {"tool": "calculate_fuel"} + +event: tool_result +data: {"tool": "calculate_fuel", "result": {"carb_g_per_hr": 72}} + +event: text_delta +data: {"delta": " for your marathon is 72g per hour"} + +event: message_end +data: {"conversation_id": "9f3c1a7e-2b48-4d05-a6c1-7e0b3d5f8a12"} +``` + +| Event type | Payload | Meaning | +|------------|---------|---------| +| `message_start` | `{"conversation_id"}` | Stream opened. On conversation creation this is where you learn the new ID | +| `text_delta` | `{"delta"}` | A chunk of the response text. Concatenate these in order | +| `tool_call` | `{"tool"}` | The AI is about to call an internal tool | +| `tool_result` | `{"tool", "result"}` | That tool's result. Informational; you do not need to act on it | +| `safety_warning` | `{"message"}` | Safety text that must be shown to the athlete, see below | +| `error` | `{"message"}` | Generation failed. The stream still closes normally | +| `message_end` | `{"conversation_id"}` | Stream complete | + +Handle unknown event types by ignoring them; more may be added. + +Text is validated before it reaches you, one sentence at a time, so a `text_delta` lags the model slightly rather than arriving token by token. Two consequences worth handling: + +- A sentence that fails validation is replaced with a bracketed redaction placeholder delivered as an ordinary `text_delta`, so the response stays readable but is not what the model wrote. +- If generation crosses an absolute safety limit, the remaining text is dropped and you receive a `safety_warning` event explaining that generation was halted. `message_end` still fires afterwards, so `message_end` alone does not mean the answer is complete. Surface `safety_warning` content to the athlete rather than discarding it, and point them at the structured prescription from the calculate endpoints for the actual numbers. + +## Tool call budgets + +The AI Coach calls internal tools to do its work, such as running a fuel calculation or searching the product database. Saturday enforces the budgets; partners neither set nor see them: + +| Budget | Limit | +|--------|-------| +| Tool calls per message turn | 10 | +| Tool calls per conversation | 100 | +| Tool calls per partner per day | 5,000 | + +When a budget is exhausted the blocked call returns an error to the model rather than failing your request, and the AI answers with what it already gathered. The tool names that appear in `tool_call` events are internal and can change; do not branch on them. + +## Wrapping Saturday AI in your AI + +If your platform already has its own AI assistant, it can act as the orchestrator: it sends the athlete's question to the AI Coach on their behalf, reads the streamed response, and incorporates it into its own output. The athlete never sees Saturday's interface, and you still get coaching grounded in their Saturday profile. + +### Constraints for AI-to-AI use + +- **Respect `not_instructions: true`.** Saturday's responses are guidance for a human to consider, not commands for automated execution. +- **Don't strip safety warnings.** If a response raises a safety concern, your AI must surface it to the athlete. +- **Don't modify prescriptions.** Present Saturday's numbers as returned rather than adjusting them. +- **Include attribution.** For teaser tier, "Powered by Saturday" must be visible to the athlete. + +## Conversation history + +Retrieve past messages for a conversation: + + + +```python Python +import os + +response = requests.get( + f"https://api.saturday.fit/v1/ai/conversations/{conv_id}/messages", + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, +) + +messages = response.json()["messages"] +for msg in messages: + print(f"[{msg['role']}] {msg['content']}") +``` + +```typescript TypeScript +const response = await fetch( + `https://api.saturday.fit/v1/ai/conversations/${convId}/messages`, + { + headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}` }, + } +); + +const { messages } = await response.json(); +messages.forEach((msg: any) => console.log(`[${msg.role}] ${msg.content}`)); +``` + + + +## Listing athlete conversations + +```bash +GET /v1/athletes/{athlete_id}/ai/conversations +``` + +Returns all conversations for an athlete with their metadata: message count, last activity, and summary. + +## Deleting a conversation + +```bash +DELETE /v1/ai/conversations/{conv_id} +``` + +Returns `204` and removes the conversation document and every message in it, which is what you call to service a GDPR erasure request. Deleting also releases the concurrency slot, so delete conversations you are done with rather than leaving them open. + +## Limits + +AI Coach endpoints carry their own limits on top of your account's standard rate limit, because each call costs far more compute than a calculate: + +| Limit | Value | +|-------|-------| +| Concurrent open conversations per partner | 10 | +| New conversations per partner per day | 1,000 | +| `message` field | 10 KB | +| Whole request body | 32 KB | + +Exceeding a conversation limit returns `429` with a `Retry-After` header and the code `conversation_limit_exceeded`. An oversized message returns `400` with `message_too_long`. + +A conversation slot is held for as long as the stream is open and released when you delete the conversation. Abandoned streams are reclaimed after two hours of inactivity, so a client that drops connections without deleting conversations will eventually hit the concurrency limit. diff --git a/error-handling.mdx b/error-handling.mdx index 9e0fbe2..f0b4607 100644 --- a/error-handling.mdx +++ b/error-handling.mdx @@ -6,20 +6,18 @@ sidebarTitle: 'Error Handling' # Error Handling -Saturday uses a consistent error format across all endpoints, modeled on Stripe's error structure. Errors are predictable, machine-readable, and include enough context to diagnose the issue without consulting logs. +Saturday uses one error format across all endpoints, modeled on Stripe's error structure. ## Error format -Every error response follows this structure: - ```json { "error": { "type": "invalid_request", - "code": "missing_required_field", - "message": "The 'athlete_weight_kg' field is required for hydration calculations.", - "param": "athlete_weight_kg", - "documentation_url": "https://api.saturday.fit/docs/errors#missing_required_field", + "code": "missing_field", + "message": "activity_type is required", + "param": "activity_type", + "documentation_url": "https://docs.saturday.fit/errors#missing_field", "request_id": "req_abc123def456" } } @@ -28,42 +26,45 @@ Every error response follows this structure: | Field | Always present | Description | |-------|---------------|-------------| | `type` | Yes | Category of error | -| `code` | Yes | Specific error code (machine-readable) | +| `code` | Yes | Specific error code, machine-readable | | `message` | Yes | Human-readable explanation | | `param` | No | The request parameter that caused the error | -| `documentation_url` | Yes | Direct link to docs for this error | -| `request_id` | Yes | Unique identifier for this request — include in support tickets | +| `documentation_url` | Yes | Docs link for this code | +| `request_id` | Yes | Identifier for this request. Include it in support tickets | ## Error types -| Type | HTTP Status | Description | -|------|-------------|-------------| -| `authentication_error` | 401 | Invalid, expired, or missing API key | -| `authorization_error` | 403 | Valid key but insufficient permissions | -| `invalid_request` | 400 | Request is malformed or missing required fields | -| `not_found_error` | 404 | The requested resource doesn't exist | -| `conflict` | 409 | Request conflicts with current state | -| `rate_limit_error` | 429 | Too many requests | -| `safety_limit` | 422 | Calculation would violate safety guardrails | -| `api_error` | 500 | Something went wrong on Saturday's end | +Branch on `type` for handling class, and on `code` for the specific case. A type can span more than one status. + +| Type | Statuses | Description | +|------|----------|-------------| +| `invalid_request` | 400, 409, 410 | Malformed body, missing or out-of-range field, or a request that conflicts with current state | +| `authentication_error` | 401, 403 | Missing or invalid key (401), revoked key or suspended account (403) | +| `authorization_error` | 403 | Valid credentials, insufficient permission, scope, tier, or feature access | +| `resource_not_found` | 404 | The resource doesn't exist, or belongs to another partner | +| `rate_limit_error` | 429 | Rate limit, daily call ceiling, or request-pattern friction | +| `api_error` | 500, 502, 503 | Saturday-side failure (500), upstream service (502), transient dependency (503) | - **Branch on `type`, not `code`.** For a 404 the `type` is `not_found_error` while the more specific `code` is `resource_not_found`; for a 429 the `type` is `rate_limit_error` while the `code` is `rate_limit_exceeded`. If you switch on `error.type`, use the values in the table above. + The `type` for a 404 is `resource_not_found`, and the `code` narrows it: `athlete_not_found`, `activity_not_found`, `key_not_found`, `webhook_not_found`. A 429 carries `type: "rate_limit_error"` with `code: "rate_limit_exceeded"` or `code: "rate_limited"`. Switching on `type` means using the values in the table above. +Safety ceilings do not surface as an error status. A prescription that runs into a guardrail returns `200` with the clamped values plus `safety.warnings` and `safety.requires_human_review` set. See [Safety](/guides/safety). + ## Retry logic -| Error type | Retry? | Strategy | +| Type | Retry? | Strategy | |------------|--------|----------| | `authentication_error` | No | Fix credentials | -| `authorization_error` | No | Fix permissions | +| `authorization_error` | No | Fix permissions, scopes, or tier | | `invalid_request` | No | Fix the request | -| `not_found_error` | No | Fix the resource ID | -| `rate_limit_error` | Yes | Wait for `Retry-After` header | -| `safety_limit` | No | Review and adjust parameters | -| `api_error` | Yes | Exponential backoff | +| `resource_not_found` | No | Fix the resource ID | +| `rate_limit_error` | Yes | Honor `Retry-After` when present, otherwise back off. See [Rate Limiting](/rate-limiting) | +| `api_error` | Yes | Exponential backoff. A 503 from key verification is transient: retry, don't rotate the key | -### Exponential backoff implementation +### Exponential backoff + +`Retry-After` is absent on some 429s, notably the daily call ceiling, so read it with a fallback rather than indexing it directly. @@ -122,4 +123,8 @@ async function requestWithRetry( ### Request IDs -Every response includes a `request_id` in both the response body and the `X-Request-Id` header. Always include this when reporting issues — it lets us trace the exact request through our systems. +Every response carries an `X-Request-Id` header, and every error body repeats it as `request_id`. Quote it when reporting an issue and we can trace that exact request. + +### Error catalog + +`GET /v1/errors` (no auth) lists the error codes for the features currently exposed to you. diff --git a/guides/activities.mdx b/guides/activities.mdx index bb639d9..2bb265c 100644 --- a/guides/activities.mdx +++ b/guides/activities.mdx @@ -1,42 +1,39 @@ --- title: 'Activities' -description: 'Activity lifecycle — creating, prescribing, and tracking' +description: 'Creating activities, prescribing for them, and recording how they went' sidebarTitle: 'Activities' icon: 'bicycle' --- # Activities -Activities represent individual training sessions or races. Each activity belongs to an athlete and can have a nutrition prescription, selected products, and post-activity feedback attached. +An activity is one training ride, run, swim, or race. Each activity belongs to an athlete and can carry a nutrition prescription and post-activity feedback. -## Activity lifecycle +Whatever your platform calls these (workouts, sessions, events), the Saturday object is an activity, and every field and route below uses that word. -``` -Create activity -> Calculate prescription -> (Optional) Add products -> (Optional) Submit feedback -``` +## Activity lifecycle -1. **Create** the activity with type, duration, and optional intensity/thermal stress -2. **Calculate** a nutrition prescription for it -3. **Add products** the athlete plans to use (optional — for preparation planning) -4. **Submit feedback** after the activity (optional — improves future prescriptions) +1. **Create** the activity with type and duration, plus intensity and thermal stress if you have them. +2. **Calculate** a nutrition prescription for it. +3. **Submit feedback** afterward, if you collect it. ## Creating an activity ```python Python +import os import requests response = requests.post( "https://api.saturday.fit/v1/athletes/ath_abc123/activities", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "type": "run", - "name": "Saturday long run", "duration_min": 120, "intensity_level": 5, - "scheduled_at": "2025-01-15T07:00:00Z", "thermal_stress_level": 4, + "external_id": "your-activity-88213", }, ) @@ -50,16 +47,15 @@ const response = await fetch( { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "run", - name: "Saturday long run", duration_min: 120, intensity_level: 5, - scheduled_at: "2025-01-15T07:00:00Z", thermal_stress_level: 4, + external_id: "your-activity-88213", }), } ); @@ -79,24 +75,18 @@ console.log(`Activity ID: ${activity.id}`); | `intensity_level` | integer | No | 1-9 scale | | `thermal_stress_level` | integer | No | 1-9 scale | | `is_race_event` | boolean | No | Whether this is a race | -| `name` | string | No | Human-readable label | -| `scheduled_at` | string | No | ISO 8601 datetime | +| `meal_before_min` | integer | No | Minutes between the athlete's last meal and the start | +| `external_id` | string | No | Your platform's activity ID | -### Multi-sport activities +Create accepts these fields and no others. There is no field for a display name or a scheduled start time; keep those on your side and join on `external_id`. -For triathlons and brick workouts, specify sub-activities: +`external_id` is worth sending. Saturday's hosted onboarding finish screen uses it to deep-link the athlete back into the matching activity in your app. -```json -{ - "type": "bike", - "name": "Olympic distance race", - "duration_min": 165, - "intensity_level": 8, - "is_race_event": true -} -``` +### Multi-sport activities + +Each activity carries one `type`, and Saturday prescribes for that type: an athlete fuels differently on the bike than on the run. For a triathlon or a brick, create one activity per leg. -Saturday calculates prescriptions tailored to the activity type — you fuel differently on the bike than on a run. +To work out which legs a multi-sport title implies, use `POST /v1/infer/brick-types`. ## Calculating a prescription @@ -104,14 +94,14 @@ Saturday calculates prescriptions tailored to the activity type — you fuel dif POST /v1/athletes/{athlete_id}/activities/{activity_id}/calculate ``` -This uses the activity's parameters and the athlete's profile to generate a personalized prescription. Recalculating overwrites the previous prescription — use this when activity parameters change (e.g., updated weather forecast). +This combines the activity's parameters with the athlete's profile. Recalculating overwrites the previous prescription, so call it again whenever the activity changes: a revised duration, a new weather forecast. The response is tier-aware, with the same semantics as `/v1/nutrition/calculate`: -- **Full tier** (subscribed, covered, or in-trial athletes): `tier: "full"` with the exact `prescription` object (carb/sodium/fluid totals and per-hour rates), stored on the activity. Trial responses also carry `tier_source: "trial"`, `trial_ends_at`, and `trial_calls_remaining_today` — each calculation debits the trial's daily call allowance, exactly like nutrition calculate. -- **Teaser tier**: `tier: "teaser"` with per-hour *ranges* (`carb_range_g_per_hr`, `sodium_range_mg_per_hr`, `fluid_range_ml_per_hr`), a `subscription_cta` with the athlete's subscribe link, and required attribution. Nothing is stored — `GET .../prescription` keeps returning the last full-tier result, if any. +- **Full tier** (subscribed, covered, or in-trial athletes): `tier: "full"` with the `prescription` object (carb, sodium, and fluid totals and per-hour rates), stored on the activity. Trial responses also carry `tier_source: "trial"`, `trial_ends_at`, and `trial_calls_remaining_today`. Each calculation debits the trial's daily call allowance, exactly like nutrition calculate. +- **Teaser tier**: `tier: "teaser"` with per-hour ranges (`carb_range_g_per_hr`, `sodium_range_mg_per_hr`, `fluid_range_ml_per_hr`), a `subscription_cta` carrying the athlete's subscribe link, and required attribution. Nothing is stored, so `GET .../prescription` keeps returning the last full-tier result if there was one. -`safety` metadata is always included regardless of tier. +`safety` metadata is included on every tier. ## Getting a stored prescription @@ -130,48 +120,58 @@ Returns the most recently calculated prescription for this activity, wrapped wit "total_carb_g": 125, "total_sodium_mg": 970, "total_fluid_ml": 1240, - "calculated_at": "2025-01-15T06:55:00Z" + "calculated_at": 1737010500 }, "safety": { - "max_safe_fluid_ml_per_hr": 1801, - "max_safe_sodium_mg_per_hr": 5000, - "confidence_score": 0.85, - "not_instructions": true, - "warnings": [] + "max_safe_fluid_ml_per_hr": 1500, + "max_safe_sodium_mg_per_hr": 3000, + "confidence_score": 0, + "requires_human_review": false, + "warnings": null, + "not_instructions": true } } ``` +`calculated_at` is a Unix timestamp in seconds, not an ISO 8601 string. Timestamps across the athlete and activity objects (`created_at`, `updated_at`) use the same encoding. + - The `{prescription, safety}` envelope is consistent across all Saturday endpoints that return prescription data. Always read `prescription.*` for values and `safety.*` for guardrail information. The `not_instructions: true` flag signals that these are recommendations for human consideration, not executable commands. + The `{prescription, safety}` envelope is the same on every Saturday endpoint that returns prescription data. Read `prescription.*` for values and `safety.*` for guardrails. The `not_instructions: true` flag marks these as recommendations for a person to consider, not commands to execute. + + The activity-scoped endpoints populate the two ceilings and `not_instructions`, but not the rest of the block: `confidence_score` comes back as `0` and `warnings` as `null` from both this read and `POST .../calculate`. Handle the null, and do not render that zero as a confidence of zero. When you need a confidence score or prescription warnings, call `POST /v1/nutrition/calculate` with the same parameters. See [Safety](/guides/safety#where-each-safety-field-is-populated). + + ## Getting an activity with prescription ```bash GET /v1/athletes/{athlete_id}/activities/{activity_id} ``` -The activity response includes the prescription inline, along with any attached products. +The activity response carries the stored prescription inline. ## Listing activities ```bash -GET /v1/athletes/{athlete_id}/activities?from=2025-01-01&to=2025-01-31&type=running +GET /v1/athletes/{athlete_id}/activities?limit=50&cursor=1737043200 ``` -Supports filtering by date range and type. +Activities come back newest first, paginated by cursor. `limit` defaults to 50 and is clamped to 1-200; pass `pagination.next_cursor` back as `cursor` for the next page. There are no date-range or type filters; filter on your side, or track activities by `external_id`. ## Submitting feedback -Post-activity feedback improves future prescriptions: +Record how the fueling went: ```python Python +import os +import requests + response = requests.post( "https://api.saturday.fit/v1/athletes/ath_abc123/activities/act_xyz789/feedback", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "rating": 4, "notes": "Felt good until mile 18, then energy dropped", @@ -185,7 +185,7 @@ const response = await fetch( { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -205,13 +205,11 @@ const response = await fetch( | `rating` | integer | Yes | Overall prescription quality, 1 (poor) to 5 (excellent) | | `notes` | string | No | Free-text feedback on how the fueling went | - - Feedback is the single most valuable signal for improving future prescriptions. The more activities with feedback, the more personalized Saturday becomes. Encourage athletes to submit feedback — even simple ratings help. - +Feedback is stored on the activity and returned with it on read, so your UI can show an athlete what they said last time and a coach can review a block of training. It does not currently feed back into the calculation: prescriptions come from the athlete's profile and the activity's parameters, and a rated activity does not change the next one. Update the profile fields to change future prescriptions. ## Activity type inference -If your platform has activity metadata but not a clean type, Saturday can infer it: +If your platform has activity metadata but not a clean type, Saturday can infer one: ```bash POST /v1/infer/activity-type @@ -226,12 +224,16 @@ POST /v1/infer/activity-type ``` Response: + ```json { - "inferred_type": "run", - "confidence": 0.95, - "intensity_hint": "easy" + "type": "run", + "confidence": 0.95 } ``` -For multi-sport titles like "Bike/run brick", use `POST /v1/infer/brick-types`. +`type` is one of the seven activity keys or `"unknown"`. Inference never fails the request: when the model is unavailable or the evidence is too thin, you get `"unknown"` with `confidence: 0`, so branch on the type rather than on an error. + +Sending more than a title sharpens the result. The endpoint also reads `pre_activity_comment`, `workout_type` (your provider's own coarse type), `velocity` in m/s, `has_power`, and `cadence`, and those structured signals resolve most cases before the model is consulted. + +For multi-sport titles like "Bike/run brick", use `POST /v1/infer/brick-types`. It returns `sub_types` (for example `["bike", "run"]`) with a single `confidence`, and you create one activity per leg. diff --git a/guides/ai-coach.mdx b/guides/ai-coach.mdx deleted file mode 100644 index 2a6c448..0000000 --- a/guides/ai-coach.mdx +++ /dev/null @@ -1,255 +0,0 @@ ---- -title: 'AI Coach Integration' -description: 'Embed Saturday AI coaching with SSE streaming in your platform' -sidebarTitle: 'AI Coach' -icon: 'robot' ---- - -# AI Coach Integration - -Saturday's AI Coach provides conversational nutrition coaching powered by Claude and Gemini. Partners can embed this coaching experience directly in their platforms — athletes get personalized nutrition advice without leaving your app. - - - The AI Coach feature is currently in **STEALTH** stage and requires explicit alpha access. Contact [api@saturday.fit](mailto:api@saturday.fit) to request access. - - -## How it works - -The AI Coach is a context-aware nutrition expert that knows each athlete's profile, activity history, fueling preferences, and past feedback. It can: - -- Answer nutrition questions specific to the athlete's situation -- Explain why a prescription was calculated the way it was -- Help plan race-day nutrition strategies -- Recommend products based on preferences and tolerances -- Provide pre-activity preparation instructions - -Communication happens via Server-Sent Events (SSE) for real-time streaming responses. - -## Conversation lifecycle - -``` -Create conversation → Send messages (SSE stream) → Get history → (Optional) Delete -``` - -### Creating a conversation - - - -```python Python -import requests - -response = requests.post( - "https://api.saturday.fit/v1/ai/conversations", - headers={"Authorization": "Bearer sk_test_abc123def456"}, - json={"athlete_id": "ath_abc123"}, -) - -conversation = response.json() -conv_id = conversation["id"] -``` - -```typescript TypeScript -const response = await fetch( - "https://api.saturday.fit/v1/ai/conversations", - { - method: "POST", - headers: { - Authorization: "Bearer sk_test_abc123def456", - "Content-Type": "application/json", - }, - body: JSON.stringify({ athlete_id: "ath_abc123" }), - } -); - -const conversation = await response.json(); -const convId = conversation.id; -``` - - - -### Sending a message (SSE streaming) - -Messages return responses via Server-Sent Events. The AI streams its response token-by-token for a real-time typing experience. - - - -```python Python -import requests - -# SSE streaming requires reading the response as a stream -response = requests.post( - f"https://api.saturday.fit/v1/ai/conversations/{conv_id}/messages", - headers={ - "Authorization": "Bearer sk_test_abc123def456", - "Content-Type": "application/json", - "Accept": "text/event-stream", - }, - json={"message": "What should I eat before my marathon on Saturday?"}, - stream=True, -) - -for line in response.iter_lines(): - if line: - decoded = line.decode("utf-8") - if decoded.startswith("data: "): - chunk = decoded[6:] # Strip "data: " prefix - if chunk == "[DONE]": - break - print(chunk, end="", flush=True) -``` - -```typescript TypeScript -const response = await fetch( - `https://api.saturday.fit/v1/ai/conversations/${convId}/messages`, - { - method: "POST", - headers: { - Authorization: "Bearer sk_test_abc123def456", - "Content-Type": "application/json", - Accept: "text/event-stream", - }, - body: JSON.stringify({ - message: "What should I eat before my marathon on Saturday?", - }), - } -); - -const reader = response.body!.getReader(); -const decoder = new TextDecoder(); - -while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - const lines = chunk.split("\n"); - - for (const line of lines) { - if (line.startsWith("data: ")) { - const data = line.slice(6); - if (data === "[DONE]") break; - process.stdout.write(data); - } - } -} -``` - - - -### SSE event format - -``` -event: token -data: The recommended - -event: token -data: carbohydrate intake - -event: tool_call -data: {"tool": "calculate_fuel", "status": "started"} - -event: tool_result -data: {"tool": "calculate_fuel", "result": {"carb_g_per_hr": 72}} - -event: token -data: for your marathon is 72g per hour - -event: done -data: [DONE] -``` - -| Event type | Description | -|------------|-------------| -| `token` | A text chunk of the AI's response | -| `tool_call` | The AI is calling an internal tool (calculation, product search, etc.) | -| `tool_result` | The result of a tool call (informational — you don't need to act on this) | -| `done` | Stream complete | - -## Tool call budgets - -The AI Coach has access to internal tools (fuel calculations, product lookups, knowledge base search). Each conversation has a per-message tool call budget to prevent runaway costs: - -| Context | Budget | Tools available | -|---------|--------|-----------------| -| Standard message | 5 calls | calculate_fuel, search_products, search_knowledge, get_athlete_settings, prep_simulation | -| Complex planning | 8 calls | All standard + compare_scenarios, batch_calculate, gear_analysis | - -Partners don't manage this budget — Saturday enforces it automatically. If the AI exhausts its budget, it provides the best answer possible with the tools it already called. - -## Wrapping Saturday AI in your AI - -If your platform already has its own AI assistant, you can wrap Saturday's coaching into it: - -### Architecture pattern - -``` -Your AI (orchestrator) → Saturday AI Coach API → Response to your AI → Your AI presents to athlete -``` - -Your AI sends questions to Saturday's AI Coach on behalf of the athlete, receives the streamed response, and incorporates it into your own AI's output. This avoids exposing Saturday's UI directly while still getting personalized nutrition coaching. - -### Important constraints for AI-to-AI - -- **Respect `not_instructions: true`** — Saturday's responses are guidance for human consideration, not commands for automated execution -- **Don't strip safety warnings** — if Saturday's AI mentions a safety concern, your AI must surface it to the athlete -- **Don't modify prescriptions** — your AI should present Saturday's numbers as-is, not adjust them -- **Include attribution** — for teaser tier, "Powered by Saturday" must be visible to the athlete - -## Conversation history - -Retrieve past messages for a conversation: - - - -```python Python -response = requests.get( - f"https://api.saturday.fit/v1/ai/conversations/{conv_id}/messages", - headers={"Authorization": "Bearer sk_test_abc123def456"}, -) - -messages = response.json()["messages"] -for msg in messages: - print(f"[{msg['role']}] {msg['content']}") -``` - -```typescript TypeScript -const response = await fetch( - `https://api.saturday.fit/v1/ai/conversations/${convId}/messages`, - { - headers: { Authorization: "Bearer sk_test_abc123def456" }, - } -); - -const { messages } = await response.json(); -messages.forEach((msg: any) => console.log(`[${msg.role}] ${msg.content}`)); -``` - - - -## Listing athlete conversations - -```bash -GET /v1/athletes/{athlete_id}/ai/conversations -``` - -Returns all conversations for an athlete, with metadata (message count, last activity, summary). - -## Deleting a conversation - -```bash -DELETE /v1/ai/conversations/{conv_id} -``` - -Permanently removes the conversation and all messages. This supports GDPR right to erasure. - -## Rate limiting for AI - -AI Coach endpoints have separate rate limits from standard API calls due to higher compute cost: - -| Limit | Value | -|-------|-------| -| Messages per minute per athlete | 10 | -| Concurrent conversations per partner | 50 | -| Max message length | 2,000 characters | - -Exceeding these limits returns a standard `429` response with `Retry-After` header. diff --git a/guides/athlete-onboarding-journey.mdx b/guides/athlete-onboarding-journey.mdx index 344df66..af80fde 100644 --- a/guides/athlete-onboarding-journey.mdx +++ b/guides/athlete-onboarding-journey.mdx @@ -5,7 +5,7 @@ sidebarTitle: 'Onboarding Journey' icon: 'route' --- -This page is a **narrative** — written so a developer (or an AI assistant building your integration) can picture the whole athlete experience and know exactly what to implement at each beat. The reference details live in [Athlete Onboarding](/guides/onboarding); this is the story. +This page is a narrative, written so a developer, or an AI assistant building your integration, can picture the whole athlete experience and know what to implement at each beat. The reference details live in [Athlete Onboarding](/guides/onboarding); this is the story. ## State machine @@ -13,47 +13,49 @@ An athlete on your platform is always in exactly one of these states: | State | What calculations return | What moves them forward | |---|---|---| -| **No profile** | Widest honest bands + onboarding invite. Trial clock NOT started. | Any single answered field | -| **Partial profile** | Narrower bands; `missing_fields` shows what's left, most-impactful first. Trial clock running. | More answers (any mechanism) | -| **Complete, in trial** | Exact numbers, up to 30 days / capped daily calls | Subscribing via the teaser CTA | -| **Complete, subscribed** | Exact numbers, always | — | -| **Complete, trial expired** | Teaser ranges + subscribe CTA | Subscribing | +| **No profile** | Widest bands, plus an onboarding invite. Trial clock not started. | Any single answered field | +| **Partial profile** | Narrower bands; `missing_fields` shows what is left, most-impactful first. Trial clock running. | More answers, by any of the four mechanisms | +| **Complete, in trial** | Exact numbers, for 30 days, within the daily call cap | Subscribing | +| **Complete, subscribed** | Exact numbers, always | Nothing left to do | +| **Complete, trial expired** | Teaser ranges plus a subscribe CTA | Subscribing | + +The subscribe CTA rides on teaser responses, so an athlete meets it when the trial expires or when they spend the day's calls, not while the trial is answering in full. ## The journey, narrated -**1. You create the athlete and run their first workout calculation.** -You've synced an athlete from your database — maybe just their name and email. You call calculate for tomorrow's 2-hour ride. Saturday answers with a band ("60-80 g carbs/hr"), `profile_complete: false`, a sorted list of missing fields, and an `onboarding.url`. Nothing is broken — this is Saturday refusing to fake precision. The athlete's trial hasn't started, so nothing is being wasted either. +**1. You create the athlete and run their first activity calculation.** +You have synced an athlete from your database, maybe just their name and email. You call calculate for tomorrow's 2-hour ride. Saturday answers with a band ("60-80 g carbs/hr"), `profile_complete: false`, a sorted list of missing fields, and an `onboarding.url`. The band is the answer, not a degraded one: Saturday will not compute an exact-looking number out of defaults. The athlete's trial has not started either, so this call costs them nothing. -*Your code:* show the band, and surface the onboarding invite — a button like "Get your exact numbers (2 min)" linking to `onboarding.url`. +*Your code:* show the band, and surface the onboarding invite, a button like "Get your exact numbers (2 min)" linking to `onboarding.url`. **2. The athlete taps the invite.** -They land on a Saturday page co-branded with your platform ("YOURAPP × SATURDAY"), greeted by first name. The page says how many questions are left and offers two equal paths: answer right here, or get the free Saturday app (signing up with the same email they use on your platform, so the accounts connect). Either path works; the page is the faster first experience. +They land on a Saturday page co-branded with your platform ("YOURAPP × SATURDAY"), greeted by first name. The page says how many questions are left and offers two paths: answer here, or get the free Saturday app, signing up with the same email they use on your platform so the accounts connect. Either works; the page is the faster first experience. -*Your code:* nothing. The page only asks what's missing — anything you already sent (sex, weight…) is skipped automatically. +*Your code:* nothing. The page asks only what is missing, and skips anything you already sent, such as sex or weight. **3. They answer one question per screen.** -Sweat level, saltiness, carb experience… the same athlete-tested questions Saturday's own app asks, as simple tap-targets. Every answer saves instantly — if they bail at question 4, those 4 answers already narrowed their bands. The link keeps working; they can come back. +Sweat level, saltiness, carb experience, the same questions Saturday's own app asks, as tap targets. Every answer saves as it is given, so bailing at question 4 still leaves those 4 answers narrowing the bands. The link keeps working and they can come back. **4. The finish moment.** -On the last answer, the page asks: *"Can we show you fuel for one of your activities?"* If they say yes and you've registered `activity_link_template`, they deep-link straight into **your** activity screen — where your integration now displays exact Saturday numbers. If not, the page shows their most recent activity's exact targets and your `fueling_path_copy` ("In YourApp: open any activity → Fuel tab"), then returns them via your `return_url`. +On the last answer, the page asks: *"Can we show you fuel for one of your activities?"* If they say yes and you have registered `activity_link_template`, they deep-link straight into your activity screen, where your integration now shows exact Saturday numbers. If not, the page shows their most recent activity's exact targets alongside your `fueling_path_copy` ("In YourApp: open any activity, then the Fuel tab"), and returns them via your `return_url`. -*Your code:* register `activity_link_template` and `fueling_path_copy` on your partner account (one-time setup) — it turns Saturday's finish screen into a hand-back into your product at its best moment. +*Your code:* register `activity_link_template` and `fueling_path_copy` on your partner account once. That is what turns Saturday's finish screen into a hand-back into your product at the moment the athlete most wants to be there. **5. The webhook fires.** -`athlete.profile_completed` arrives exactly once. Their next calculation — and every one after — returns exact numbers. The 30-day trial is running; the teaser CTA handles conversion after that ([Freemium Model](/guides/freemium-model)). +`athlete.profile_completed` arrives once. Their next calculation, and every one after, returns exact numbers, as long as the call also carries the activity's intensity, thermal stress, meal timing, and race flag. The 30-day trial is running; the subscribe CTA handles conversion after that ([Freemium Model](/guides/freemium-model)). -*Your code:* on that webhook, refresh any cached athlete state and consider an in-app moment: "Your fueling numbers are now exact." +*Your code:* on that webhook, refresh any cached athlete state, and consider marking the moment in your UI: "Your fueling numbers are now exact." ## The app path, narrated -Some athletes already use (or will prefer) the Saturday app. When their app account email matches the email you sent on the athlete record, Saturday links them and their full app onboarding feeds your calculations automatically — live, not synced, nothing for you to build. Their app answers are never exposed to you through the API; if they flip "share my fueling profile with \{your platform\}" in app settings, profile values become visible to you as well. +Some athletes already use, or will prefer, the Saturday app. When their app account email matches the email you sent on the athlete record, Saturday links them and their app onboarding feeds your calculations, resolved live rather than synced, with nothing for you to build. Their app answers are never exposed to you through the API. If they turn on "share my fueling profile with \{your platform\}" in app settings, the profile values become visible to you as well. -*Tell your athletes:* "use the same email as your \{platform\} account" — that one sentence prevents the only silent failure in this path. +*Tell your athletes:* "use the same email as your \{platform\} account". Matching is exact apart from case and whitespace, so a different address never links, and no error surfaces anywhere for you to catch. ## What to build, minimally 1. Show `precision.message` and the invite button whenever `profile_complete` is false. -2. Register `activity_link_template` + `fueling_path_copy` (one-time). -3. Handle `athlete.profile_completed` (optional but delightful). +2. Register `activity_link_template` and `fueling_path_copy` once. +3. Handle `athlete.profile_completed`, if you want to mark the moment in your UI. -That's the whole integration. Everything else is Saturday's job. +Saturday handles the questions, the hosted page, the linking, and the checkout. diff --git a/guides/athletes.mdx b/guides/athletes.mdx index db72496..7340e78 100644 --- a/guides/athletes.mdx +++ b/guides/athletes.mdx @@ -7,25 +7,29 @@ icon: 'person-running' # Athletes -Athletes are the core entity in Saturday's API. Each athlete has a profile with physical characteristics and fueling preferences that personalize their nutrition prescriptions. +Athletes are the core entity in Saturday's API. Each athlete has a profile of physical characteristics and fueling preferences that personalize their nutrition prescriptions. -Athletes are **partner-scoped** — your organization can only access athletes created through your API key. There is no cross-partner data access. +Athletes are partner-scoped: your organization can only reach athletes created through your API key. There is no cross-partner data access. ## Creating an athlete ```python Python +import os import requests response = requests.post( "https://api.saturday.fit/v1/athletes", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "external_id": "your-user-12345", "name": "Alex Runner", + "email": "alex@example.com", + "sex": "male", + "year_of_birth": 1990, "weight_kg": 70, - "fitness_level": "intermediate", + "settings": {"sweat_level": 7, "saltiness": 5}, }, ) @@ -37,14 +41,17 @@ print(f"Saturday ID: {athlete['id']}") const response = await fetch("https://api.saturday.fit/v1/athletes", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ external_id: "your-user-12345", name: "Alex Runner", + email: "alex@example.com", + sex: "male", + year_of_birth: 1990, weight_kg: 70, - fitness_level: "intermediate", + settings: { sweat_level: 7, saltiness: 5 }, }), }); @@ -56,59 +63,74 @@ console.log(`Saturday ID: ${athlete.id}`); ### External IDs -The `external_id` field maps the Saturday athlete to your platform's user. This is your user ID, not Saturday's. Use it to look up athletes without storing Saturday's `ath_` IDs. +The `external_id` field maps the Saturday athlete to your platform's user. This is your user ID, not Saturday's. Use it to look athletes up without storing Saturday's own IDs. ## Profile fields -All fields except `weight_kg` are optional. More fields produce more personalized prescriptions. +Every field is optional at create time. The athlete's fueling profile is what makes prescriptions exact, so the fields below split into two groups: the ones the engine needs before it will return exact numbers, and the ones that narrow the band further. An athlete missing any safety-core field gets a range instead of a number. See [Athlete Onboarding](/guides/onboarding). -### Required +### Top-level fields | Field | Type | Description | |-------|------|-------------| -| `weight_kg` | number | Body weight in kilograms | - -### Recommended - -| Field | Type | Description | -|-------|------|-------------| -| `fitness_level` | string | `beginner`, `intermediate`, `advanced`, `elite` | -| `primary_sport` | string | Main activity type | - -### Personalization settings - -These fields significantly improve prescription accuracy: - -| Field | Type | Description | -|-------|------|-------------| -| `sweat_level` | string | `low`, `moderate`, `heavy`, `very_heavy` | -| `saltiness` | string | `low`, `moderate`, `salty`, `very_salty` | -| `satiety_level` | string | `low`, `moderate`, `high` — fullness during exercise | -| `carb_tolerance` | string | `low`, `moderate`, `high` — gut tolerance for carbs | -| `carb_upper_limit_override` | number | Max carbs (g/hr) the athlete wants | -| `concerns` | array | `"vegan"`, `"gluten_free"`, `"caffeine_sensitive"`, etc. | +| `external_id` | string | Your platform's user ID | +| `name` | string | Display name | +| `email` | string | Enables [automatic linking](/guides/freemium-model#automatic-linking-by-email) to an existing Saturday subscriber | +| `sex` | string | `male`, `female`, or `intersex` | +| `year_of_birth` | integer | Used to derive age | +| `weight_kg` | number | Body weight, 20 to 250 | +| `settings` | object | The fueling profile, below | +| `partner_plan` | string | Set to `annual` to assert bundle-offer eligibility | +| `org_id` | string | Organization affiliation; must reference an org you created | + +Saturday sets `id`, `partner_id`, `created_at`, `updated_at`, `profile_complete`, and `subscription_status`. Writes to those are ignored. + +### Settings: the fueling profile + +`settings` holds the fields the engine reads. The 1-9 scales are odd-point selectors, not continuous sliders: send 1, 3, 5, 7, or 9. + +| Field | Type | Values | Safety-core | +|-------|------|--------|-------------| +| `sweat_level` | integer | 1 (light) to 9 (heavy), default 5 | Yes | +| `saltiness` | integer | 1 (not salty) to 9 (very salty), default 5 | Yes | +| `carb_experience` | string | `range_0_30`, `range_40_60`, `range_gt_70` | Yes | +| `usual_carb_consumption` | string | `range_lt_60`, `range_60_80`, `range_80_100`, `range_gt_100` | Yes | +| `satiety_level` | integer | 1 (mostly whole food) to 9 (high-octane fuel), default 5 | No | +| `fitness_level` | integer | 1 (just starting) to 9 (elite), default 5 | No | +| `carb_upper_limit_override` | integer | 50 to 150 g/hr, default 150 | No | + +`carb_experience` is the most carbohydrate per hour the athlete has ever fueled with; `usual_carb_consumption` is what they typically take. Both are stored as range tokens rather than numbers, because a range is what an athlete can answer without guessing. + +### Fueling concerns + +Concerns are individual booleans inside `settings`, not a list of tags: + +| Field | Meaning | +|-------|---------| +| `muscle_cramps` | Prone to cramping during exercise | +| `gut_distress` | Prone to GI distress | +| `performance` | Prioritizing performance over comfort | +| `hunger` | Prone to hunger during exercise | +| `heat_tolerance` | Poor heat tolerance | +| `faintness` | Prone to feeling faint | +| `drinking_resistance` | Resistant to drinking during exercise | +| `thirst` | Excessive thirst during exercise | + +Sending any concern key marks the concerns question answered, including when you send it as `false`. An athlete who has never been asked and an athlete who answered "none of these" are different states to the precision engine, and only the second one counts as complete. - **Settings evolve over time.** As athletes train their gut and build tolerance, settings should be updated. Encourage periodic review of `carb_tolerance` and `sweat_level`. + Settings change as athletes train their gut and their sweat rate adapts. A profile answered once and never revisited drifts. Re-ask `carb_experience`, `usual_carb_consumption`, and `sweat_level` periodically. -### Sensitive fields - -| Field | Type | Description | -|-------|------|-------------| -| `eating_disorder_flag` | boolean | Triggers additional safety guardrails and adjusted language | - - - The `eating_disorder_flag` exists to protect vulnerable athletes. When set, Saturday avoids triggering language around food restriction, caloric deficit, or weight. Never expose this flag in partner UIs — it's a backend safety signal. - - ## Listing athletes ```bash -GET /v1/athletes?limit=20&offset=0 +GET /v1/athletes?limit=50&cursor=1737043200 ``` -Returns a paginated list of athletes for your organization. +Pagination is cursor-based, not offset-based. `limit` defaults to 50 and is clamped to 1-200. The response carries `pagination.next_cursor`; pass it back as `cursor` for the next page, and stop when `pagination.has_more` is false. + +Add `?profile_complete=false` to list only the athletes whose profiles are still short of exact numbers. That is your nudge list. ## Updating an athlete @@ -116,19 +138,18 @@ Returns a paginated list of athletes for your organization. PATCH /v1/athletes/{id} ``` -Only include fields you want to change. Unspecified fields are not modified. +Include only the top-level fields you want to change. Unspecified top-level fields are not modified. ```python Python +import os +import requests + response = requests.patch( "https://api.saturday.fit/v1/athletes/ath_abc123def456", - headers={"Authorization": "Bearer sk_test_abc123def456"}, - json={ - "weight_kg": 72, - "sweat_level": "heavy", - "carb_tolerance": "high", - }, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, + json={"weight_kg": 72, "email": "alex@example.com"}, ) ``` @@ -138,39 +159,57 @@ const response = await fetch( { method: "PATCH", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, - body: JSON.stringify({ - weight_kg: 72, - sweat_level: "heavy", - carb_tolerance: "high", - }), + body: JSON.stringify({ weight_kg: 72, email: "alex@example.com" }), } ); ``` +### Updating the fueling profile + +```bash +PATCH /v1/athletes/{id}/settings +``` + +The settings object is written as a unit. Send the athlete's complete settings on every write, including the values you are not changing, or the omitted ones are cleared. Read the current values first with `GET /v1/athletes/{id}/settings` and merge on your side. + +The same replacement rule applies if you send a `settings` object through `PATCH /v1/athletes/{id}`. + +Saturday rejects `carb_upper_limit_override` outside 50-150 g/hr and `weight_kg` outside 20-250 with a 400 rather than silently clamping, so a bad value in your data surfaces at the boundary instead of distorting a prescription. + ## Deleting an athlete +Two endpoints delete, and they are not interchangeable. + ```bash DELETE /v1/athletes/{id} ``` +Removes the athlete document only. Their activities, prescriptions, and feedback stay in storage. + +```bash +POST /v1/athletes/{id}/delete +``` + +The erasure path. It deauthorizes connected providers, purges the athlete's subcollections (activities, conversations, consent, integrations), and files the request the data warehouse honors. + - Athlete deletion is permanent and cascading. All activities, prescriptions, and feedback are deleted. This supports GDPR right to erasure but cannot be undone. + Use `POST /v1/athletes/{id}/delete` for a GDPR erasure request. `DELETE` alone leaves the athlete's activity data behind. Neither can be undone. ## Data export (GDPR) -Export all data for an athlete: +Export all data held for an athlete: ```bash POST /v1/athletes/{id}/export ``` -Returns a JSON file containing the athlete's complete profile, all activities, prescriptions, and feedback. +Returns JSON containing the athlete's profile, every activity with its prescription and feedback, any AI coach conversations, and their consent records. ## Settings schema @@ -180,8 +219,6 @@ Fetch the global settings schema to build dynamic forms: GET /v1/settings/schema ``` -Returns field definitions with valid ranges, descriptions, and validation rules — useful for building settings UIs without hardcoding Saturday's requirements. This is a global, public schema (the same for every athlete); fetch an individual athlete's current values with `GET /v1/athletes/{id}/settings`. +Returns each field's type, valid range or option list, description, and default, so a settings UI does not have to hardcode Saturday's requirements. This is a global schema, identical for every athlete; fetch one athlete's current values with `GET /v1/athletes/{id}/settings`. - -**Graduated precision (2026-06):** every calculation response now carries a `precision` object — `profile_complete`, impact-sorted `missing_fields`, and an onboarding invite URL. Exact numbers require a complete fueling profile; incomplete profiles get honest bands (never wider than teaser ranges). See [Athlete Onboarding](/guides/onboarding). - +For collecting the profile from scratch rather than editing it, `GET /v1/onboarding/questions` returns the same fields as athlete-facing questions with copy, ordering, and answer values. See [Athlete Onboarding](/guides/onboarding). diff --git a/guides/attribution.mdx b/guides/attribution.mdx index e5ace22..6fc0dec 100644 --- a/guides/attribution.mdx +++ b/guides/attribution.mdx @@ -43,10 +43,9 @@ Two rules on the words: - **Link the mark** to `https://saturday.fit` (or to the `subscribe_url` from a teaser response, so the athlete lands on their own upgrade path). A linked mark is how a curious athlete finds full - precision. That link is the whole deal: you get free nutrition intelligence, Saturday gets a way - to reach the athlete. + precision, and it is what Saturday gets in exchange for the nutrition intelligence you get free. - **Plain text is a valid fallback.** Where a logo will not fit, appropriately sized text reading - "Powered by Saturday" satisfies the requirement. You never have an excuse not to attribute. + "Powered by Saturday" satisfies the requirement. Do not put "Saturday" in your product's name, and do not imply Saturday developed, sponsored, or endorsed your app. Truthful, factual references to Saturday in your feature descriptions are fine. @@ -77,9 +76,14 @@ device data), honor that source's attribution too. Name the origin, not only the ## How it should look -Saturday provides the "Powered by Saturday" mark in light and dark variants, stacked and -horizontal, as a drop-in asset. Stacked is the primary form; use horizontal where vertical -space is limited. Use the asset as shipped. +Saturday provides the "Powered by Saturday" mark stacked and horizontal, each in two variants, as a +drop-in asset. Stacked is the primary form; use horizontal where vertical space is limited. Use the +asset as shipped. + +The variant is named for the background it sits on, not for the artwork inside it. The +`-light` files carry dark artwork for a light background, and the `-dark` files carry light artwork +for a dark background. Choose from the background directly behind the mark, not from the viewer's +device theme. Powered by Saturday @@ -90,11 +94,11 @@ space is limited. Use the asset as shipped. the plain-text form. - **Clear space:** keep open space around the mark equal to at least half the mark's own height on every side. This scales with the mark, so there is one rule at every size. -- **Contrast:** use the light mark on dark backgrounds and the dark mark on light. Keep it legible. +- **Contrast:** keep the mark legible against whatever sits behind it, using the variant rule above. - **Do not** recolor, restretch, rotate, animate, add effects to, or rebuild the mark, and do not use - it, or any part of it, as your app icon or avatar. -- **Keep your brand dominant.** The Saturday mark appears near your own name but stays separate from - it and never larger or more prominent than your own branding. Present, not loud. + it, or any part of it, as your app icon or avatar. Preserve the original aspect ratio. +- **Keep your brand dominant.** The Saturday mark appears near your own name, stays separate from + it, and is never larger or more prominent than your own branding. Do not rename or rebrand Saturday's fueling terms and metrics. Call them what Saturday calls them so athletes carry one vocabulary across every app they use. @@ -113,26 +117,29 @@ Saturday data. Both can appear on one screen doing different jobs. ## Data license -Attribution rides alongside the data-use terms every response already carries in its headers -(`X-Saturday-Data-License`, `X-Saturday-Data-Training`, `X-Saturday-Data-Attribution`). Saturday's -prescriptions, product database, and knowledge base are proprietary: no training or fine-tuning of -models on response data, no reverse-engineering the calculations, no reselling or building a derived -database. Full terms: [Data Policy](/guides/data-policy). +Attribution rides alongside the data-use terms every response already carries in its +`X-Saturday-Data-License` header, with `X-Saturday-Data-Attribution` added on teaser responses. +Saturday's prescriptions, product database, and knowledge base are proprietary: no training or +fine-tuning of models on response data, no reverse-engineering the calculations, no reselling or +building a derived database. Full terms: [Data Policy](/guides/data-policy). ## If your use is commercial Putting Saturday in front of other people commercially (a platform, an app in a store, a coaching -business charging clients, anything that resells or bundles Saturday output) gets a quick brand -review before launch. Send mockups or screenshots of the surfaces that show Saturday data to -[api@saturday.fit](mailto:api@saturday.fit) and we confirm the attribution reads well. It is a short -step, not a gate on every build, and it is only for new display surfaces. +business charging clients, anything that resells or bundles Saturday output) gets a brand review +before launch. Send mockups or screenshots of the surfaces that show Saturday data to +[api@saturday.fit](mailto:api@saturday.fit) and we confirm the attribution reads well. The review +applies to each new display surface, not to every build. -Personal, single-account use skips this. Build freely. +Personal, single-account use skips this. ## Example +Both examples sit on a light background, so both use the `-light` variant. Swap to `-dark` on a dark +surface. + ```html Web

Carbohydrates: 60-80 g/hr

@@ -140,7 +147,7 @@ Personal, single-account use skips this. Build freely.

Sodium: 300-600 mg/hr

- Powered by Saturday
@@ -155,7 +162,10 @@ Column( Text('Sodium: 300-600 mg/hr'), GestureDetector( onTap: () => launchUrl(Uri.parse(subscribeUrl)), - child: SvgPicture.asset('assets/powered-by-saturday-stacked.svg', height: 40), + child: SvgPicture.asset( + 'assets/brand/powered-by-saturday-stacked-light.svg', + height: 40, + ), ), ], ) diff --git a/guides/batch-operations.mdx b/guides/batch-operations.mdx index 0dbf21a..707bea4 100644 --- a/guides/batch-operations.mdx +++ b/guides/batch-operations.mdx @@ -9,6 +9,8 @@ icon: 'layer-group' Batch endpoints let you perform multiple operations in a single API call. Use these when you need to process a training week, onboard a team, or import historical activities. +The examples read your key from a `SATURDAY_API_KEY` environment variable, as in [Quickstart](/quickstart). Sandbox keys are issued with their own base URL; using one against `api.saturday.fit` returns `401 invalid_api_key`. + ## Batch calculate Calculate prescriptions for multiple scenarios at once. Ideal for building "training week" views or "what if" comparisons. @@ -17,84 +19,61 @@ Calculate prescriptions for multiple scenarios at once. Ideal for building "trai POST /v1/nutrition/calculate/batch ``` +Each scenario is a full calculate request, so `athlete_id` goes on the scenario, not at the top level. Results come back in request order, and there is no per-scenario label: match results to inputs by position. + ```python Python +import os import requests +ATHLETE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" +week = [ + {"activity_type": "run", "duration_min": 45, "intensity_level": 3}, + {"activity_type": "run", "duration_min": 60, "intensity_level": 7}, + {"activity_type": "bike", "duration_min": 180, "intensity_level": 5, + "thermal_stress_level": 7}, +] + response = requests.post( "https://api.saturday.fit/v1/nutrition/calculate/batch", - headers={"Authorization": "Bearer sk_test_abc123def456"}, - json={ - "athlete_id": "ath_abc123", - "scenarios": [ - { - "label": "Monday easy run", - "activity_type": "run", - "duration_min": 45, - "intensity_level": 3, - }, - { - "label": "Wednesday intervals", - "activity_type": "run", - "duration_min": 60, - "intensity_level": 7, - }, - { - "label": "Saturday long ride", - "activity_type": "bike", - "duration_min": 180, - "intensity_level": 5, - "thermal_stress_level": 7, - }, - ], - }, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, + json={"scenarios": [dict(s, athlete_id=ATHLETE) for s in week]}, ) -results = response.json() -for result in results["results"]: - print(f"{result['label']}: {result['carb_g_per_hr']}g carbs/hr") +data = response.json() +for i, result in enumerate(data["results"]): + print(f"scenario {i}: {result['carb_g_per_hr']}g carbs/hr") +for err in data.get("errors", []): + print(f"scenario {err['index']} failed: {err['code']} {err['message']}") ``` ```typescript TypeScript +const ATHLETE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"; +const week = [ + { activity_type: "run", duration_min: 45, intensity_level: 3 }, + { activity_type: "run", duration_min: 60, intensity_level: 7 }, + { activity_type: "bike", duration_min: 180, intensity_level: 5, + thermal_stress_level: 7 }, +]; + const response = await fetch( "https://api.saturday.fit/v1/nutrition/calculate/batch", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ - athlete_id: "ath_abc123", - scenarios: [ - { - label: "Monday easy run", - activity_type: "run", - duration_min: 45, - intensity_level: 3, - }, - { - label: "Wednesday intervals", - activity_type: "run", - duration_min: 60, - intensity_level: 7, - }, - { - label: "Saturday long ride", - activity_type: "bike", - duration_min: 180, - intensity_level: 5, - thermal_stress_level: 7, - }, - ], + scenarios: week.map((s) => ({ ...s, athlete_id: ATHLETE })), }), } ); const data = await response.json(); -data.results.forEach((r: any) => - console.log(`${r.label}: ${r.carb_g_per_hr}g carbs/hr`) +data.results.forEach((r: any, i: number) => + console.log(`scenario ${i}: ${r.carb_g_per_hr}g carbs/hr`) ); ``` @@ -102,37 +81,53 @@ data.results.forEach((r: any) => ### Response format +Counts and the request ID sit at the top level; there is no `metadata` object. Each entry in `results` is the same body the single calculate endpoint returns, so it carries `tier`, `safety`, and `attribution` and does not carry an index. + ```json { "results": [ { - "index": 0, - "label": "Monday easy run", + "tier": "full", "carb_g_per_hr": 30, "sodium_mg_per_hr": 300, "fluid_ml_per_hr": 400, - "safety": { "confidence_score": 0.65, "warnings": [], "not_instructions": true } + "safety": { + "max_safe_fluid_ml_per_hr": 1000, + "max_safe_sodium_mg_per_hr": 1500, + "confidence_score": 0.65, + "requires_human_review": false, + "warnings": [], + "not_instructions": true + } }, { - "index": 1, - "label": "Wednesday intervals", + "tier": "full", "carb_g_per_hr": 55, "sodium_mg_per_hr": 400, "fluid_ml_per_hr": 500, - "safety": { "confidence_score": 0.65, "warnings": [], "not_instructions": true } + "safety": { + "max_safe_fluid_ml_per_hr": 1000, + "max_safe_sodium_mg_per_hr": 1500, + "confidence_score": 0.65, + "requires_human_review": false, + "warnings": [], + "not_instructions": true + } } ], "errors": [], - "metadata": { - "total_scenarios": 3, - "successful": 3, - "failed": 0, - "request_id": "req_abc123def456" - } + "total": 3, + "succeeded": 3, + "failed": 0, + "estimated_ms": 4200, + "elapsed_ms": 4381, + "request_id": "req_ca5a6125aad2" } ``` -If individual scenarios fail, they appear in `errors` with the index and error detail. Successful scenarios are still returned — batch operations don't fail atomically. +Failed scenarios appear in `errors` as `{ "index", "code", "message" }`, and the successful ones are still returned: batch operations do not fail atomically. + +Because failures are dropped from `results` rather than nulled, `results` is shorter than `scenarios` when anything fails, and a result's position no longer matches its scenario index. When you need that mapping and failures are possible, read the failed indexes from `errors` first and reconstruct alignment from them. ### Limits @@ -141,6 +136,12 @@ If individual scenarios fail, they appear in `errors` with the index and error d | Max scenarios per batch | 50 | | Rate limiting | Counts as 1 API call per scenario | +Exceeding 50 returns `400` with the code `batch_too_large`, and an empty `scenarios` array returns `400` with `empty_batch`. + +### Sizing the wait + +A batch takes as long as the sum of its scenarios. Two things let you show real progress instead of a spinner: the response carries an `X-Batch-Estimated-Ms` header, flushed before processing begins, and sending `"estimate_only": true` returns the same estimate immediately without running any calculations, consuming quota, or returning results. + ## Bulk athlete create Onboard multiple athletes in one call. Useful for team imports or platform migrations. @@ -149,12 +150,16 @@ Onboard multiple athletes in one call. Useful for team imports or platform migra POST /v1/athletes/batch ``` +This endpoint takes a partner API key only. An athlete-delegated OAuth token is rejected up front with `403` rather than partway through, so a scope mistake never leaves some athletes created and others not. + ```python Python +import os + response = requests.post( "https://api.saturday.fit/v1/athletes/batch", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "athletes": [ { @@ -191,7 +196,7 @@ for athlete in results["created"]: const response = await fetch("https://api.saturday.fit/v1/athletes/batch", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -225,7 +230,15 @@ results.created.forEach((a: any) => console.log(`Created: ${a.name} -> ${a.id}`) | Constraint | Value | |------------|-------| | Max athletes per batch | 100 | -| Duplicate `external_id` | Error for that item, others continue | +| Minimum per athlete | At least one of `name`, `email`, or `external_id` | + +Each athlete is also validated on `weight_kg` (must be positive), `year_of_birth` (1900 to the current year), and `sex` (`male`, `female`, or `intersex`). A violation fails that item only. + +The batch is also checked against your account's total athlete quota before processing. If you are at the cap the whole request returns `403` with the code `resource_limit`, rather than partially filling. + + + **`external_id` is not deduplicated.** Saturday does not reject or merge an athlete whose `external_id` you have already used; every create mints a new athlete with a new UUID. Retrying a batch that partially succeeded will therefore create duplicates of the athletes that succeeded the first time. Track the returned IDs against your own `external_id` values and retry only the items that failed. + ## Activity import @@ -235,28 +248,45 @@ Import multiple activities for an athlete at once. Useful for backfilling histor POST /v1/athletes/{athlete_id}/activities/import ``` +An imported activity accepts these fields and no others. Unknown keys are ignored silently, so check this list rather than assuming a field was stored: + +| Field | Required | Description | +|-------|----------|-------------| +| `type` | Yes | One of `bike`, `run`, `swim`, `row`, `ski`, `lift`, `hike` | +| `duration_min` | Yes | Positive integer | +| `intensity_level` | No | 1 to 10 | +| `thermal_stress_level` | No | 1 to 10 | +| `is_race_event` | No | Boolean | +| `external_id` | No | Your own activity ID, carried through and used by finish-screen deep links | +| `calculate` | No | Calculate a prescription for this activity, see below | + + + There is no timestamp field on import. Imported activities are stamped with the time Saturday created them, so a backfill does not preserve the original activity dates. Keep your own date mapping via `external_id` if you need it. + + ```python Python +import os + response = requests.post( - f"https://api.saturday.fit/v1/athletes/ath_abc123/activities/import", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + "https://api.saturday.fit/v1/athletes/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activities/import", + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "activities": [ { "type": "run", - "name": "New Year's Day 10K", "duration_min": 48, "intensity_level": 7, - "scheduled_at": "2025-01-01T09:00:00Z", "thermal_stress_level": 2, + "is_race_event": True, + "external_id": "strava-10k-2025-01-01", }, { "type": "bike", - "name": "Weekend group ride", "duration_min": 150, "intensity_level": 5, - "scheduled_at": "2025-01-04T08:00:00Z", + "external_id": "strava-group-ride-2025-01-04", }, ], }, @@ -265,29 +295,28 @@ response = requests.post( ```typescript TypeScript const response = await fetch( - `https://api.saturday.fit/v1/athletes/ath_abc123/activities/import`, + "https://api.saturday.fit/v1/athletes/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activities/import", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ activities: [ { type: "run", - name: "New Year's Day 10K", duration_min: 48, intensity_level: 7, - scheduled_at: "2025-01-01T09:00:00Z", thermal_stress_level: 2, + is_race_event: true, + external_id: "strava-10k-2025-01-01", }, { type: "bike", - name: "Weekend group ride", duration_min: 150, intensity_level: 5, - scheduled_at: "2025-01-04T08:00:00Z", + external_id: "strava-group-ride-2025-01-04", }, ], }), @@ -297,6 +326,8 @@ const response = await fetch( +Up to **200 activities** per import. Exceeding that returns `400` with `batch_too_large`. The import is also checked against the athlete's activity quota before processing, returning `403` with `resource_limit` if it would exceed the cap. + ### Calculating prescriptions during import Set `calculate: true` at the top level to calculate a prescription for every imported activity, or on individual activities to calculate selectively: @@ -311,37 +342,44 @@ Set `calculate: true` at the top level to calculate a prescription for every imp } ``` -Each calculation runs the same tier-aware path as the single calculate endpoint: full-tier and in-trial athletes get exact prescriptions (stored on the activity and mirrored on `imported[].prescription`); teaser-tier athletes get per-hour ranges with a `subscription_cta`. Trial athletes debit their daily call allowance per calculated activity — a large import can use up the day's allowance mid-batch, after which remaining items return teaser ranges. +Each calculation runs the same tier-aware path as the single calculate endpoint: full-tier and in-trial athletes get exact prescriptions, stored on the activity and mirrored on `imported[].prescription`, while teaser-tier athletes get per-hour ranges with a `subscription_cta`. Trial athletes debit their daily call allowance per calculated activity, so a large import can exhaust the day's allowance mid-batch, after which the remaining items return teaser ranges. -Per-item outcomes ride a `prescriptions` array in the response (`index`, `activity_id`, and the tier-aware `result`). A failed calculation never fails the import — the activity is still created, and the failure appears on that item's `code`/`message`. +Per-item outcomes ride a `prescriptions` array in the response, each with `index`, `activity_id`, and the tier-aware `result`. A failed calculation never fails the import: the activity is still created, and the failure appears on that item's `code` and `message`. -Imports also count against rate limits per item, like batch calculate. +Imports count against rate limits per item, like batch calculate. ## Error handling in batch operations -Batch operations use partial success semantics. If 3 out of 5 items succeed and 2 fail: +Batch operations use partial success semantics. If 3 of 5 items succeed and 2 fail, the 3 successes are committed, the 2 failures are returned in `errors`, and the HTTP status is `200` rather than `400` because some items succeeded. -- The 3 successful items are committed -- The 2 failed items are returned in the `errors` array with index, error type, and message -- The HTTP status is `200` (not `400`) because some items succeeded +Every batch response uses the same envelope. The success array is named for the operation (`results`, `created`, or `imported`), and alongside it sit `errors`, `total`, `succeeded`, `failed`, and `request_id`. Each error is a flat object with `index`, `code`, and `message`: ```json { "created": [ - { "index": 0, "id": "ath_abc123", "name": "Alice Runner" }, - { "index": 2, "id": "ath_def456", "name": "Carol Triathlete" } + { + "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "partner_id": "your-partner-id", + "name": "Alice Runner", + "external_id": "user-001", + "weight_kg": 58, + "created_at": 1765467600 + } ], "errors": [ { "index": 1, - "error": { - "type": "conflict", - "code": "duplicate_external_id", - "message": "An athlete with external_id 'user-002' already exists" - } + "code": "invalid_value", + "message": "weight_kg must be positive" } - ] + ], + "total": 3, + "succeeded": 2, + "failed": 1, + "request_id": "req_ca5a6125aad2" } ``` -Check both `created` (or `results`) and `errors` arrays to handle the response correctly. +Read both arrays. `index` on an error refers to the item's position in your request, which is the only reliable way to tell which input failed. + +Unlike the resource IDs above, `request_id` is not a UUID: it is `req_` followed by 12 hex characters. Log it. It is what support needs to trace a specific call. diff --git a/guides/coach-api.mdx b/guides/coach-api.mdx index ab16b1a..5e2856d 100644 --- a/guides/coach-api.mdx +++ b/guides/coach-api.mdx @@ -7,12 +7,16 @@ icon: 'users' # Coach API -The Coach API exposes a coach's Saturday surface — the per-athlete fueling rollup, AI reports, the roster's needs-attention markers, and the **entire notifications/alerting control center** — over a coach-scoped REST API (and an [MCP connector](/guides/coach-connector)). +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 the power-user coach who wants to automate fueling monitoring: read who needs attention, pull a report, and configure every alert rule and digest across a whole roster — programmatically. +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. - The Coach API requires the **Business tier or higher**. A coach whose tier no longer includes API access degrades cleanly to their own athlete data only — coach endpoints return `404` until an eligible tier is restored. There is no error and no separate "downgrade" call. + **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. + + + + **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`. ## Authentication @@ -24,34 +28,36 @@ Two ways to authenticate as a coach. Both resolve to the same coach identity and | **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 **[Settings → API Keys](https://coach.saturday.fit/admin/api-keys)**. Pass the key as a Bearer token: +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 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`). The same OAuth token also acts as an athlete-self token on `/v1/athletes/*` — a coach is also an athlete. +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 & capabilities +## Scopes and capabilities -The coach's token/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`. +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` | `GET /v1/coach/roster`, `…/fueling-rollup`, `…/sessions/…` | -| Read reports + digest | `coach:reports` | `roster:read` | `…/report`, `/v1/coach/roster/digest` | -| Write alert + report config | `coach:alerts` | `roster:write` | `/v1/coach/config/*` | +| 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. + - **Roster confinement.** Every `{athlete_uid}` you pass is checked against your roster. An athlete who isn't on your roster (or doesn't exist) returns `404 resource_not_found` — the two cases are deliberately indistinguishable, so the API never reveals whether an athlete exists. + **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. ## Reads ### List the roster -`GET /v1/coach/roster` — every athlete you coach, each with a calm needs-attention summary over the look-back window. +`GET /v1/coach/roster` returns every athlete you coach, each with a needs-attention summary over the look-back window. ```bash curl -H "Authorization: Bearer cp_live_..." \ @@ -78,11 +84,11 @@ curl -H "Authorization: Bearer cp_live_..." \ } ``` -The markers use Saturday's **one shared concern definition** — so they match the coach portal table and the digest exactly. An athlete who fueled well shows `flagged: false` and is never alarmed. +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` — the same data, but **only** athletes who crossed a concern bar this window, most-flagged first. Calm by design: the silent majority is omitted. Ideal for a large roster ("who needs my attention this week?"). +`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 { @@ -90,13 +96,23 @@ The markers use Saturday's **one shared concern definition** — so they match t "window": 7, "flagged_count": 3, "total_count": 48, - "flagged": [ /* RosterEntry[], most-flagged first */ ] + "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` — one athlete's in-window sessions (the same table the portal renders) plus the concern summary and the resolved cutoffs that produced the markers. +`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 | |-------------|--------|---------| @@ -108,8 +124,29 @@ The markers use Saturday's **one shared concern definition** — so they match t "athlete_uid": "ath_123", "window": 14, "focus": "rolling", - "sessions": [ /* per-session adherence: carb/sodium/fluid fractions, totals, symptoms, rating, sleep */ ], - "concern": { /* the AthleteConcern summary off the one definition */ }, + "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", @@ -122,11 +159,13 @@ The markers use Saturday's **one shared concern definition** — so they match t } ``` -Every missing value stays `null` (never a misleading `0`). +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` — the AI-generated fueling report: a calm, third-person **narrative** grounded only in the athlete's real numbers, plus the **structured concern summary** behind it (so an agent can quote the prose or compute on the data — [API-9](/guides/data-policy)). +`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 | |-------------|--------|-------| @@ -141,7 +180,11 @@ Every missing value stays `null` (never a misleading `0`). "window": 14, "focus": "rolling", "narrative": "Over the last two weeks, this athlete …", - "concern": { /* structured summary */ }, + "concern": { + "flagged": true, + "flagged_count": 2, + "top_reasons": ["Sodium 57%", "1 symptom"] + }, "generated_at": 1749500000000, "latest_session_ms": 1749480000000, "from_cache": true @@ -152,21 +195,21 @@ Served from cache unless a newer session has landed or `refresh=true`. ### Session detail -`GET /v1/coach/athletes/{athlete_uid}/sessions/{activity_id}` — drill into one session: the full per-session projection (planned-vs-actual fueling, symptoms, vessel reports, weather, sleep) plus the concern markers that session crossed. +`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 -The Coach API is primarily a **configuration surface**. Anything a coach can configure in the portal — channels, triggers, per-nutrient thresholds, the overall→group→athlete scope hierarchy, consolidation, cadence, quiet hours, AI-report defaults — is configurable here. The portal UI and the API are two views of one config model. +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. - **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 > group > overall). `scope_id` is required for `group`/`athlete` and omitted for `overall`. + **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`. ### Notification rules -`GET /v1/coach/config/notification-rules?scope=overall` reads the rules set at exactly that scope (not the merged resolution). +`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. This is the headline tool — it's an **idempotent upsert**: re-running with the same body is a no-op (there's no per-request idempotency key needed). +`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 curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ @@ -192,15 +235,15 @@ curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ **Channels:** `in_portal`, `email`, `push`, `webhook`. (SMS is not yet supported.) -**Cadence:** `realtime` (urgent, real-time) or `digest` (bundled into one daily item per athlete). +**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 to use the resolved concern cutoff). +**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 2-trigger ANDs: both legs must be markers on the **same** session ("sodium short AND a cramp"). Exactly two triggers; no OR/NOT/nesting. +**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 — a one-shot "set up my whole roster" you can then tweak. +`POST /v1/coach/config/preset` applies a named starting point at a scope, which you can then tweak rule by rule. ```bash curl -X POST https://api.saturday.fit/v1/coach/config/preset \ @@ -235,14 +278,18 @@ curl -X PUT https://api.saturday.fit/v1/coach/config/report-settings \ ``` - **Athlete data is read-only.** The Coach API never writes an athlete's fueling data, debriefs, or prescriptions. A coach can only configure their **own** alerts, reports, groups, and thresholds. + **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. ## Webhooks -A webhook is "just another delivery channel" on the alerts you already configure (`"channels": ["webhook"]`). Register an endpoint, then select `webhook` as a channel on any rule. +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. -`POST /v1/coach/webhooks` registers an endpoint and returns the signing secret **once**. + + 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. + + +`POST /v1/coach/webhooks` registers an endpoint and returns the signing secret once. ```bash curl -X POST https://api.saturday.fit/v1/coach/webhooks \ @@ -259,10 +306,14 @@ curl -X POST https://api.saturday.fit/v1/coach/webhooks \ "events": ["concern.detected", "athlete.needs_attention"], "active": true, "created_at": 1749500000000, - "secret": "whsec_…" // returned ONCE — store it + "secret": "whsec_…" } ``` + + `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. + + | Method · path | Action | |---------------|--------| | `GET /v1/coach/webhooks` | list endpoints (secrets never returned) | @@ -271,15 +322,15 @@ curl -X POST https://api.saturday.fit/v1/coach/webhooks \ | `POST /v1/coach/webhooks/{id}/disable` | disable (stop delivery, keep the endpoint) | | `POST /v1/coach/webhooks/{id}/enable` | re-enable | -**Event types:** `concern.detected`, `athlete.needs_attention`. The URL must be public `https` (internal/loopback URLs are rejected). Deliveries are signed with HMAC-SHA256 (`X-Saturday-Signature` header) using the secret returned at registration, retried with backoff, and auto-disabled after repeated failures. See [Webhooks](/guides/webhooks) for verifying signatures. +**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 use **full athlete identity** — names and data exactly as the coach sees them in the portal. The coach owns the coaching relationship, so there is no de-identification layer in the API (the export feature's identity levels are a portal/export concern). You are responsible for downstream PII handling. See the [data & privacy policy](/guides/data-policy). +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 rate limits are generous (UX-first) — a 500-athlete roster pull or digest works without artificial paging penalties. Limits exist only for abuse protection. See [rate limiting](/rate-limiting). +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 @@ -297,7 +348,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 +// wh.secret is returned once; store it now ``` ```python Python @@ -310,7 +361,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 +# wh["secret"] is returned once; store it now ``` diff --git a/guides/coach-connector.mdx b/guides/coach-connector.mdx index f514f30..0145db6 100644 --- a/guides/coach-connector.mdx +++ b/guides/coach-connector.mdx @@ -1,16 +1,16 @@ --- title: 'Claude Connector' -description: 'Connect Saturday to Claude — one connector, athlete and coach tools' +description: 'Connect Saturday to Claude: one connector, athlete and coach tools' sidebarTitle: 'Claude Connector' icon: 'plug' --- # Saturday Claude Connector -Saturday ships a **remote MCP connector** for [Claude](https://claude.ai). One connector, one URL, one directory listing — the tools that light up depend on **who signs in**: +Saturday publishes a remote MCP connector for [Claude](https://claude.ai). There is one connector and one URL; which tools appear depends on who signs in. - An **athlete** (any active Saturday subscriber) gets self-tools over their own data. -- A **coach** (Business tier or higher) *additionally* gets roster + config tools. +- A **coach** (Pro Coach tier or above) additionally gets roster and config tools. A coach is also an athlete, so a coach sees both tool sets. An athlete-only subscriber never sees the coach tools. @@ -25,30 +25,30 @@ https://api.saturday.fit/mcp Claude auto-discovers Saturday's authorization server (RFC 9728 / RFC 8414 discovery), walks you through **"Sign in with Saturday,"** and shows a branded consent screen. A coach granting access sees an explicit disclosure that Claude will be able to read their athletes' fueling data and manage their alert settings. - **Subscriber wall.** Only an active Saturday subscriber can complete the connect flow. A non-subscriber sees a clean "subscription required" page — with a **Subscribe** link to plans and a **"Use a different account"** option (to re-authenticate if they signed in with the wrong account) — and no account side-effects. A lapsed subscriber loses access within ~1 hour (on the next token refresh). + **Subscriber wall.** Only an active Saturday subscriber can complete the connect flow. A non-subscriber sees a "subscription required" page carrying a **Subscribe** link to plans and a **"Use a different account"** option for re-authenticating after signing in with the wrong account. Nothing is written to their account. A lapsed subscriber loses access within about an hour, on the next token refresh (access tokens live one hour). -### Transport & protocol +### Transport and protocol -The connector speaks the MCP **Streamable HTTP** transport (`POST /mcp`) and protocol revision **2025-11-25** (it negotiates down to older revisions a client offers). Tokens are bound to the connector resource (`https://api.saturday.fit/mcp`) via [RFC 8707](/guides/oauth2#resource-indicators-rfc-8707) audience binding — a token minted for Saturday can only be used against Saturday. +The connector speaks the MCP Streamable HTTP transport (`POST /mcp`) and protocol revision 2025-11-25, negotiating down to older revisions a client offers. Tokens are bound to the connector resource (`https://api.saturday.fit/mcp`) via [RFC 8707](/guides/oauth2#resource-indicators-rfc-8707) audience binding, so a token minted for Saturday cannot be replayed against another server. ## Athlete tools -When an athlete connects, Claude can read and (carefully) write their own Saturday data. Athlete tools operate strictly on the signed-in athlete — there is no athlete selector. The full catalog is in [MCP Integration](/guides/mcp-integration#tool-catalog); highlights: +When an athlete connects, Claude can read and write their own Saturday data. Athlete tools operate strictly on the signed-in athlete; there is no athlete selector. The full catalog is in [MCP Integration](/guides/mcp-integration#tool-catalog). Highlights: -- `get_athlete`, `update_athlete` — read/update their own profile. -- `list_activities`, `get_activity`, `create_activity` — manage their own activities. -- `calculate_activity_prescription`, `get_activity_prescription` — Saturday's engine computes the prescription (it is never writable by hand). -- `build_bottling_plan`, `record_bottling_choice` — turn the prescription into a bottle-by-bottle mix plan; `build_bottling_plan` renders the interactive [Bottle Builder app](/guides/mcp-integration#bottle-builder-interactive-mcp-app). +- `get_athlete`, `update_athlete` read and update their own profile. +- `list_activities`, `get_activity`, `create_activity` manage their own activities. +- `calculate_activity_prescription`, `get_activity_prescription` ask Saturday's engine to compute the prescription. It is never writable by hand. +- `build_bottling_plan`, `record_bottling_choice` turn the prescription into a bottle-by-bottle mix plan. `build_bottling_plan` renders the interactive [Bottle Builder app](/guides/mcp-integration#bottle-builder-interactive-mcp-app). - `calculate_nutrition`, `search_products`, `analyze_product_fit`, `get_athlete_insights`, `search_knowledge`. - Prescriptions come only from Saturday's calculator engine. The connector can request a calculation but can never write prescription numbers — the same safety invariant as the [partner API](/guides/safety). + Prescriptions come only from Saturday's calculator engine. The connector can request a calculation but can never write prescription numbers, the same safety invariant as the [partner API](/guides/safety). ## Coach tools -A Business+ coach additionally sees the tools below. Every athlete argument is confined to the coach's roster (a non-roster athlete returns "resource not found"). These mirror the [Coach REST API](/guides/coach-api) one-to-one. +A Pro Coach or above additionally sees the tools below. Every athlete argument is confined to the coach's roster (a non-roster athlete returns "resource not found"). They cover the same operations as the [Coach REST API](/guides/coach-api), with one gap: the REST surface can disable and re-enable a webhook endpoint, and the connector cannot. ### Read tools @@ -67,36 +67,40 @@ A Business+ coach additionally sees the tools below. Every athlete argument is c | Tool | Args | Behavior | |------|------|----------| | `get_notification_rules` | `scope?`, `scope_id?` | read the rules at one scope | -| `set_notification_rules` | `rules`, `scope?`, `scope_id?` | **replace** the rules at a scope (idempotent upsert) | +| `set_notification_rules` | `rules`, `scope?`, `scope_id?` | replace the rules at a scope (idempotent upsert) | | `apply_alert_preset` | `preset`, `scope?`, `scope_id?` | apply `hands_off` / `balanced` / `hands_on` | | `get_report_settings` | `scope?`, `scope_id?` | read AI-report + concern settings | | `set_report_settings` | `settings`, `scope?`, `scope_id?` | upsert report window/focus + concern cutoffs | -| `list_webhooks` | — | list webhook endpoints (no secrets) | +| `list_webhooks` | none | list webhook endpoints (no secrets) | | `register_webhook` | `url`, `events?` | register an endpoint; returns the signing secret once | | `delete_webhook` | `webhook_id` | delete an endpoint | -`scope` is `overall` (whole roster), `group` (a coach group), or `athlete` (one athlete) — most-specific wins. `scope_id` is required for `group`/`athlete`. +`scope` is `overall` (whole roster), `group` (a coach group), or `athlete` (one athlete), and the most specific scope wins. `scope_id` is required for `group` and `athlete`. -Config writes are **idempotent by design** — `set_notification_rules` replaces the rule set at a scope, so re-running the same call is a no-op. This is why the MCP path needs no idempotency key. +Config writes are idempotent: `set_notification_rules` replaces the rule set at a scope, so re-running the same call is a no-op, and the MCP path needs no idempotency key. -## The headline use case + + Webhook delivery requires the Business or Enterprise tier. `register_webhook` succeeds below that line, but nothing is delivered until the tier qualifies. + + +## Configuring a roster in one conversation -The connector is primarily a **configuration surface**, not just a read surface. A coach can describe their monitoring philosophy in plain English and let Claude configure the whole roster: +The connector writes config as well as reading data. A coach can describe their monitoring philosophy in plain English and let Claude configure the whole roster: > *"Only ping me when sodium is under 60% on long rides for my elite group; bundle everyone else into a Friday digest, and POST concern alerts to my system."* Claude translates that into: -1. `apply_alert_preset` `{ scope: "overall", preset: "balanced" }` — a calm baseline for everyone. +1. `apply_alert_preset` `{ scope: "overall", preset: "balanced" }` sets a baseline for everyone. 2. `set_notification_rules` `{ scope: "group", scope_id: "grp_elite", rules: { notification_rules: { under_fuel: { enabled: true, urgent_threshold: 0.6, channels: ["webhook"], cadence: "realtime" } } } }`. -3. `register_webhook` `{ url: "https://my-system.example.com/saturday" }` — and store the returned secret. +3. `register_webhook` `{ url: "https://my-system.example.com/saturday" }`, then store the returned secret. ## Lapsed coach -If a coach's tier lapses mid-session, the coach tools quietly disappear on the next entitlement check — but the athlete self-tools remain (they're still a subscriber). No error, no broken state: a clean degrade to "just my own data." Webhook deliveries to an unentitled coach stop. +If a coach's tier lapses mid-session, the coach tools disappear on the next entitlement check while the athlete self-tools remain, since the coach is still a subscriber. The result is a degrade to their own data, with no error and no broken state. Webhook deliveries to an unentitled coach stop. ## See also -- [Coach API](/guides/coach-api) — the REST surface these tools mirror. -- [OAuth2](/guides/oauth2) — the connector sign-in flow, coach scopes, and RFC 8707 audience binding. -- [MCP Integration](/guides/mcp-integration) — the full tool catalog and partner-key MCP usage. +- [Coach API](/guides/coach-api) covers the REST surface behind these tools. +- [OAuth2](/guides/oauth2) covers the connector sign-in flow, coach scopes, and RFC 8707 audience binding. +- [MCP Integration](/guides/mcp-integration) has the full tool catalog and partner-key MCP usage. diff --git a/guides/data-policy.mdx b/guides/data-policy.mdx index 8ebef19..f27c1c4 100644 --- a/guides/data-policy.mdx +++ b/guides/data-policy.mdx @@ -7,7 +7,7 @@ icon: 'scale-balanced' # Data Policy -This page covers how partners may and may not use data returned by Saturday's API. These rules protect athletes, protect Saturday's intellectual property, and ensure the API ecosystem remains trustworthy. +This page covers how partners may and may not use data returned by Saturday's API. The rules exist to protect athletes and Saturday's intellectual property. ## Data license @@ -23,7 +23,7 @@ Partners may use API response data for: ### Prohibited uses -Partners may NOT use API response data for: +Partners may not use API response data for: - **Training** machine learning or AI models - **Fine-tuning** language models or other generative AI systems @@ -34,18 +34,17 @@ Partners may NOT use API response data for: - **Creating** derivative databases from product catalog data - **The training prohibition is explicit and serious.** Saturday's prescriptions are the product of 15 years of coaching expertise, a PhD in Sport Physiology, and months of algorithm development. Using API responses to train a competing model is grounds for immediate termination. + Saturday's prescriptions are the product of 15 years of coaching expertise and a PhD in Sport Physiology. Using API responses to train a competing model is grounds for immediate termination. ## Response headers -Every API response includes data policy headers: +| Header | Sent on | Value | +|--------|---------|-------| +| `X-Saturday-Data-License` | Every response, including errors and 404s | `Response data is licensed for display to end users only. Use for ML/AI training, model fine-tuning, data aggregation, reverse engineering, or redistribution is prohibited.` | +| `X-Saturday-Data-Attribution` | Teaser-tier responses | `required` | -| Header | Value | Meaning | -|--------|-------|---------| -| `X-Saturday-Data-License` | `display-only` | Data may only be displayed to end users | -| `X-Saturday-Data-Attribution` | `required` or `optional` | Whether "Powered by Saturday" is required | -| `X-Saturday-Data-Training` | `prohibited` | ML/AI training on response data is prohibited | +The license header is set before authentication runs, so it appears on rate-limit rejections and auth failures too. Match on the header's presence rather than parsing its text, which may be reworded. ## Attribution requirements @@ -57,7 +56,7 @@ When displaying teaser/free response data, partners must show "Powered by Saturd - **Link**: Attribution must link to `saturday.fit` or the subscription `subscribe_url` - **Mark**: use the "Powered by Saturday" logo where it fits; appropriately sized plain text is a valid fallback -The `X-Saturday-Data-Attribution: required` header signals this programmatically. +Teaser responses carry `X-Saturday-Data-Attribution: required`, and repeat the same terms in the response body's `attribution` object. ### Full tier (required, lighter) @@ -71,7 +70,7 @@ Full placement, sizing, and per-surface rules: [Brand & Attribution](/guides/att ## Safety disclaimer -Partners must include a visible disclaimer in their application stating that nutrition prescriptions are **guidance, not medical instructions**. Saturday provides sports nutrition recommendations based on physiological models — not medical advice. +Partners must include a visible disclaimer in their application stating that nutrition prescriptions are **guidance, not medical instructions**. Saturday provides sports nutrition recommendations based on physiological models, which are not medical advice. ### Required disclaimer language @@ -119,10 +118,10 @@ Saturday retains athlete data only for as long as the partner account is active, Saturday's curated product database (186+ endurance nutrition products) is competitive intellectual property. The following protections apply: -- **All product queries require an `athlete_id`** — no anonymous product browsing -- **Per-athlete rate limits** on product endpoints -- **No bulk export** — product listing endpoints are paginated with a maximum page size -- **All product access is logged** with partner_id, athlete_id, and query +- **All product queries require an `athlete_id`.** There is no anonymous product browsing. +- **Per-athlete rate limits** apply on product endpoints. Queries past the limit are served with a progressive delay rather than an error, so a legitimate burst still works and a crawl does not. +- **No bulk export.** Product listing endpoints are cursor-paginated with a maximum page size of 10. +- **All product access is logged** with partner_id, athlete_id, and the query. Systematic scraping, automated enumeration, or bulk extraction of the product database is grounds for immediate account suspension. diff --git a/guides/deployment.mdx b/guides/deployment.mdx index fef8897..5607a67 100644 --- a/guides/deployment.mdx +++ b/guides/deployment.mdx @@ -1,143 +1,97 @@ --- title: 'Deployment & Configuration' -description: 'DNS setup, deployment process, and AI agent discoverability' +description: 'Where the docs and the API live, and the machine-readable endpoints for AI agents' sidebarTitle: 'Deployment' icon: 'globe' --- # Deployment & Configuration -This page documents how api.saturday.fit is configured, the DNS setup, and how the documentation site is deployed. +Saturday runs two hostnames. This page says which is which, and lists the machine-readable endpoints an agent can fetch. -## Domain setup: api.saturday.fit +## The two hostnames -The API documentation is hosted on Mintlify at `api.saturday.fit`. This requires a Cloudflare CNAME record pointing to Mintlify's hosting infrastructure. +| Host | What it serves | +|------|----------------| +| `docs.saturday.fit` | This documentation site, hosted on Mintlify | +| `api.saturday.fit` | The API itself, including the MCP endpoint | -### Cloudflare DNS configuration +Requesting a docs path on `api.saturday.fit` returns the API's `404`, not a page. `GET https://api.saturday.fit/v1/` returns a discovery document whose `documentation` link points back here. + +### DNS | Record type | Name | Target | Proxy | |------------|------|--------|-------| -| CNAME | `api` | `cname.mintlify.com` | DNS only (no proxy) | +| CNAME | `docs` | `cname.mintlify-dns.com` | DNS only (no proxy) | - Cloudflare proxy must be **disabled** (DNS only / gray cloud) for the Mintlify CNAME. Mintlify handles SSL termination and needs direct DNS resolution. + Cloudflare proxy stays **disabled** (DNS only, grey cloud) on the Mintlify CNAME. Mintlify terminates SSL and needs direct DNS resolution. -### Verification steps - -After adding the CNAME: - -1. Verify DNS propagation: `dig api.saturday.fit CNAME` -2. Expected result: `api.saturday.fit. CNAME cname.mintlify.com.` -3. Navigate to `https://api.saturday.fit` — should show the documentation site -4. SSL certificate should be issued automatically by Mintlify +To verify a DNS change: `dig docs.saturday.fit CNAME` should answer `cname.mintlify-dns.com.`, `https://docs.saturday.fit` should load this site, and Mintlify issues the certificate on its own. ## Deploying documentation updates -Mintlify supports Git-based deployments. When documentation source files are updated: - -1. Push changes to the repository -2. Mintlify detects changes to `docs/mintlify/` contents -3. Site rebuilds automatically (typically under 60 seconds) -4. Live at api.saturday.fit - -### Manual deployment - -If needed, trigger a manual deployment from the Mintlify dashboard: - -1. Log in to [dashboard.mintlify.com](https://dashboard.mintlify.com) -2. Select the Saturday API project -3. Click "Deploy" to trigger a rebuild +Mintlify deploys from Git. Push to the `docs` repository's default branch and the site rebuilds, typically in under a minute. -## AI agent discoverability +A rebuild can also be triggered by hand from [dashboard.mintlify.com](https://dashboard.mintlify.com) on the Saturday API project. -Saturday publishes machine-readable context files for AI agents and LLMs: +## Machine-readable endpoints ### llms.txt ``` -URL: https://api.saturday.fit/llms.txt +https://docs.saturday.fit/llms.txt ``` -A brief overview of Saturday's API capabilities, authentication, and key endpoints. Follows the [llms.txt standard](https://llmstxt.org/). - -**Contents include:** -- What Saturday does (one-paragraph summary) -- Authentication method (Bearer token) -- Core endpoints with brief descriptions -- Safety model summary -- Link to full documentation +A short index of the API: what Saturday does, how authentication works, the core endpoints, the safety model, and a link to the full documentation. Follows the [llms.txt standard](https://llmstxt.org/). It is hand-curated in the docs repository. ### llms-full.txt ``` -URL: https://api.saturday.fit/llms-full.txt -``` - -Complete API documentation in a single text file, optimized for LLM context windows. Includes all endpoint descriptions, request/response schemas, code examples, safety documentation, and error catalogs. - -### OpenAPI spec - -``` -URL: https://api.saturday.fit/openapi.yaml +https://docs.saturday.fit/llms-full.txt ``` -The machine-readable OpenAPI 3.0 specification that drives the interactive API playground and auto-generated reference pages. +The whole documentation corpus as one text file, for loading into an LLM context window. Mintlify generates it from the MDX sources on every build, so it is never edited or committed by hand. ### MCP server ``` -URL: https://api.saturday.fit/mcp +https://api.saturday.fit/mcp ``` -The Model Context Protocol server endpoint for AI agents. See [MCP Integration](/guides/mcp-integration) for setup instructions. +The Model Context Protocol endpoint. It accepts `POST` only; a `GET` answers `405`. See [MCP Integration](/guides/mcp-integration) for setup, and the [Claude Connector](/guides/coach-connector) for the hosted connector. -## Mintlify configuration +## Site configuration -The documentation site is configured via `mint.json` in the `docs/mintlify/` directory. Key configuration: +The site is configured by `docs.json` at the root of the docs repository, against the [Mintlify `docs.json` schema](https://mintlify.com/docs.json). -| Setting | Value | Notes | -|---------|-------|-------| -| Primary color | `#1aabb8` (Saturday teal) | Sourced from brand design system | -| Dark background | `#0d1e23` (navy-darkest) | Matches website dark theme | -| Heading font | Bitter | Matches saturday.fit website | -| Body font | Inter | Matches saturday.fit website | -| OpenAPI source | `../api/openapi.yaml` | Auto-generates API reference pages | -| API playground | Enabled (simple mode) | Interactive try-it from docs | +| Setting | Value | +|---------|-------| +| Theme | `mint` | +| Primary color | `#1aabb8` (Saturday teal) | +| Light / dark accent | `#8FC5CE` / `#0e7e8a` | +| Tabs | Coaching, API Guides | +| Contextual actions | Copy, view, open in Claude, open in Cursor | ## Content structure ``` -docs/mintlify/ - mint.json # Site configuration +docs/ + docs.json # Site configuration and navigation + llms.txt # Hand-curated LLM index introduction.mdx # Landing page - quickstart.mdx # 5-min getting started - authentication.mdx # Auth guide - error-handling.mdx # Error format - rate-limiting.mdx # Rate limits - guides/ - nutrition-calculation.mdx # Core product - athletes.mdx # Athlete management - activities.mdx # Activity lifecycle - freemium-model.mdx # Teaser vs full - safety.mdx # Safety (FIRST-CLASS) - ai-coach.mdx # AI Coach SSE - webhooks.mdx # Webhook setup - oauth2.mdx # OAuth2 PKCE - organizations.mdx # Team management - batch-operations.mdx # Batch endpoints - mcp-integration.mdx # MCP for AI agents - feature-gates.mdx # Launch stages - data-policy.mdx # Legal/terms - deployment.mdx # This page - api-reference/ - (auto-generated from OpenAPI spec) - images/ - logo-dark.svg - logo-light.svg - favicon.svg + access.mdx # Getting a key + quickstart.mdx + authentication.mdx + error-handling.mdx + rate-limiting.mdx + coaching/ # Coaching tab: onboarding, billing, roster, team, nutrition + guides/ # API Guides tab: core, integration, coach API, platform + snippets/ # Shared JSX components + images/ # Logos, favicon, brand marks ``` ## Status page -Saturday publishes system status at [status.saturday.fit](https://status.saturday.fit) (when available). Partners can subscribe to status notifications for proactive awareness of maintenance or incidents. +Saturday publishes system status at [status.saturday.fit](https://status.saturday.fit) when it is available. That hostname does not currently resolve; until it does, reach [api@saturday.fit](mailto:api@saturday.fit) about an incident. diff --git a/guides/feature-gates.mdx b/guides/feature-gates.mdx index 4c217d2..22e4750 100644 --- a/guides/feature-gates.mdx +++ b/guides/feature-gates.mdx @@ -7,13 +7,9 @@ icon: 'toggle-on' # Feature Gates -Saturday's API features progress through launch stages. This system controls feature visibility, access, and stability guarantees as the platform matures. +Saturday's API features progress through launch stages. The stage controls a feature's visibility, who can reach it, and what stability you can expect from it. -## Why feature gates? - -Not all API features ship at the same time. When you integrate Saturday, some features may be in early access (alpha), some in public beta, and some fully stable (GA). Feature gates tell you exactly what to expect from each feature. - -If an endpoint returns a `404` when you expect it to work, check the feature stage — it may be in STEALTH or require alpha access. +Not all API features ship at the same time. When you integrate Saturday, some features are in early access (alpha), some in public beta, and some stable (GA). If an endpoint returns a `404` when you expect it to work, check the feature stage: it may be in STEALTH or require alpha access. ## Launch stages @@ -21,43 +17,44 @@ If an endpoint returns a `404` when you expect it to work, check the feature sta |-------|-------------|---------------|-----------| | **STEALTH** | Endpoint returns 404 (indistinguishable from nonexistent) | Nobody | Not available | | **ALPHA** | Works for allowlisted partners | Specific partners by invitation | May change without notice | -| **BETA** | Works for all authenticated partners | All partners | May have breaking changes with notice | +| **BETA** | Works for all authenticated partners | All partners | Breaking changes may occur with 14 days notice | | **GA** | Production-stable | All partners | Stable, backward compatible | -| **DEPRECATED** | Still works but being phased out | All partners | 12-month sunset notice | +| **DEPRECATED** | Still works but being phased out | All partners | Sunset date announced per feature | ### STEALTH -Features in stealth are completely invisible. The endpoint returns a standard `404` that is byte-identical to a request for a path that doesn't exist. No auth headers, no CORS headers, no logging. This is intentional — stealth features don't exist yet from the outside world. +A stealth feature is invisible. The endpoint returns a `404` byte-identical to a request for a path that doesn't exist: same body, same headers. The request never reaches authentication, CORS, or request logging, so no timing or header difference distinguishes a gated endpoint from a nonexistent one. ### ALPHA -Alpha features are available to specific partners who have been invited. If you try to access an alpha feature without being on the allowlist, you'll get a `403` response: +Alpha features are available to partners who have been invited. Reaching an alpha feature without being on the allowlist returns a `403`: ```json { "error": { "type": "authorization_error", - "code": "feature_alpha_access_required", - "message": "This feature is in alpha. Contact api@saturday.fit to request access.", - "documentation_url": "https://api.saturday.fit/docs/feature-gates" + "code": "feature_alpha", + "message": "The products feature is in alpha. Contact api-support@saturday.fit for early access.", + "documentation_url": "https://docs.saturday.fit/errors#feature_alpha", + "request_id": "req_a1b2c3d4e5f6" } } ``` -When you are on the alpha allowlist, responses include a notice header: +The response carries `X-Saturday-Feature-Stage: ALPHA` whether or not you are on the allowlist. On the allowlist, it also carries the notice header: ```http X-Saturday-Feature-Stage: ALPHA -X-Saturday-Alpha-Notice: This feature is in alpha and may change without notice. Contact api@saturday.fit with feedback. +X-Saturday-Alpha-Notice: This feature is in alpha. Behavior may change without notice. Report issues to api-support@saturday.fit. ``` ### BETA -Beta features work for all authenticated partners. They may have breaking changes with advance notice. Responses include: +Beta features work for all authenticated partners. Responses include: ```http X-Saturday-Feature-Stage: BETA -X-Saturday-Beta-Notice: This feature is in beta. Breaking changes may occur with 14 days notice. +X-Saturday-Beta-Notice: This feature is in beta. Breaking changes may occur with 14 days notice. Pin your integration to the current behavior and monitor the changelog. ``` ### GA (General Availability) @@ -66,60 +63,62 @@ GA features are production-stable with backward compatibility guarantees. Breaki ### DEPRECATED -Deprecated features still work but will be removed. A 12-month notice period starts when deprecation is announced: +Deprecated features still work but will be removed. Responses carry: ```http X-Saturday-Feature-Stage: DEPRECATED Deprecation: true ``` -## Current feature stages +The `Deprecation` header follows [RFC 8594](https://www.rfc-editor.org/rfc/rfc8594). Sunset dates are announced per feature; ask at [api@saturday.fit](mailto:api@saturday.fit) if a feature you depend on is marked deprecated. + +## The feature keys -| Feature | What it controls | Stage | -|---------|-----------------|-------| +Five keys carry a stage. Each covers the routes listed beside it. + +| Feature | What it controls | Shipped default | +|---------|-----------------|-----------------| | `nutrition` | Fuel/hydration/electrolyte calculation, prep, inference | ALPHA | -| `products` | Curated product database (186+ products) | STEALTH | +| `products` | Curated product database (186+ products) | ALPHA | | `gear` | Gear management CRUD (bottles, flasks) | STEALTH | | `ai_coach` | AI coaching with SSE streaming | STEALTH | | `ai_data` | AI-generated insights, churn risk, ML bias | STEALTH | - - Feature stages are updated in Saturday's configuration without requiring a deploy. Stage changes propagate within 5 minutes. - + + The right-hand column is the default the gateway falls back to, not a statement about production. Each environment stores its own stages in configuration, and a per-partner override can raise a single partner above the global stage. Read `GET /v1/` for what your key can actually reach today. + + +Stage changes are made in configuration rather than by deploying, and propagate within 5 minutes. ## Feature groups -Features are organized into groups for coordinated stage transitions: +Features are grouped so a stage transition can move several at once: | Group | Features | Example transition | |-------|----------|-------------------| -| `core` | nutrition, products | "Move core to BETA" — all partners get nutrition + products | -| `advanced` | ai_coach, ai_data | "Move advanced to ALPHA" — select partners get AI features | -| `all` | All 5 features | "Move all to GA" — launch day | +| `core` | nutrition, products | "Move core to BETA" gives all partners nutrition and products | +| `advanced` | ai_coach, ai_data | "Move advanced to ALPHA" gives selected partners the AI features | +| `all` | All 5 features | "Move all to GA" | ## Getting alpha access Alpha access is by invitation. To request access: 1. Email [api@saturday.fit](mailto:api@saturday.fit) with your platform name, use case, and expected volume -2. Saturday vets the request and adds your partner ID to the alpha allowlist +2. Saturday reviews the request and adds your partner ID to the alpha allowlist 3. Within 5 minutes, your API key works on alpha-stage endpoints -Alpha partners get: -- Early access to new features before public beta -- Direct support channel for integration questions -- Influence on API design decisions -- No additional cost +Alpha partners get early access to new features before public beta, a direct support channel for integration questions, and a say in API design decisions. There is no additional cost. ## Checking feature availability -The API root endpoint shows which features are available at each stage: +The API root endpoint shows what your key can reach: ```bash GET /v1/ ``` -The response includes feature discovery information. Features in STEALTH are intentionally omitted — they don't appear in discovery, error catalogs, or documentation. +The response lists the reachable endpoints, marking alpha-stage ones. Features in STEALTH are omitted from discovery, from the error catalog, and from this documentation. ## Response headers reference diff --git a/guides/freemium-model.mdx b/guides/freemium-model.mdx index b897b60..e6a94ea 100644 --- a/guides/freemium-model.mdx +++ b/guides/freemium-model.mdx @@ -7,25 +7,23 @@ icon: 'gift' # Freemium Model -Saturday's API is free for partners to integrate. The monetization happens at the athlete level — athletes subscribe to Saturday for full-precision nutrition data. - -``` -Partner integrates (free) -> Athletes see teasers (free) -> Athletes subscribe for precision -> Everyone wins -``` +Monetization happens at the athlete level: athletes subscribe to Saturday for full-precision nutrition data, and your integration is what puts the choice in front of them. Platform partners integrate at no cost; the self-serve lane has its own plan. See [Access](/access) for which lane you are in. ## Teaser vs. full comparison | | Teaser (free) | Full (subscribed) | |---|---|---| -| **Carbohydrates** | Range: `"carb_range_g_per_hr": "60-80"` | Exact: `"carb_g_per_hr": 62.5` | -| **Hydration** | Range: `"fluid_range_ml_per_hr": "600-900"` | Exact: `"fluid_ml_per_hr": 620` | -| **Sodium** | Range: `"sodium_range_mg_per_hr": "300-600"` | Exact: `"sodium_mg_per_hr": 485` | -| **Products** | Category only ("gel") | Specific products + schedule | +| **Carbohydrate** | Range: `"carb_range_g_per_hr": "60-90"` | Exact: `"carb_g_per_hr": 62.5` | +| **Fluid** | Range: `"fluid_range_ml_per_hr": "500-1000"` | Exact: `"fluid_ml_per_hr": 620` | +| **Sodium** | Range: `"sodium_range_mg_per_hr": "200-500"` | Exact: `"sodium_mg_per_hr": 485` | +| **Attribution** | Required | Optional | +| **Subscribe CTA** | Present | Absent | | **Safety metadata** | Full | Full | -| **Confidence score** | Shown | Shown | + +Teaser ranges are bucketed to a fixed grid: 30 g/hr for carbohydrate, 500 units/hr for sodium and fluid. The bucket, not the underlying number, is what the athlete sees. - **Safety is never gated.** Both teaser and full responses include complete safety metadata. Safety information is always free. + Safety is never gated. Teaser and full responses carry the same safety block. ## Detecting response type @@ -69,7 +67,13 @@ if (data.tier === "teaser") { ## 30-day full-precision trial -Every athlete's first calculate request starts a **30-day trial of full-precision responses — 15 calls on the first UTC day (exploration allowance), then 5 calls per athlete per UTC day**. During the trial, full responses carry trial metadata so you can build countdown UX: +Each athlete gets a 30-day window of full-precision responses, capped at 15 calls on the UTC day the trial starts and 5 calls per UTC day after that. + +The clock starts on that athlete's first calculation carrying any real profile or activity data, not on athlete creation and not on a zero-data call. A coach pre-loading a roster spends nothing, and neither does a calculation made before you have collected anything. Collect the fueling profile first ([Athlete Onboarding](/guides/onboarding)) and the window delivers exact numbers from day one instead of wide bands. + +A trial that starts late in a UTC day gets a short first day; the cap boundary is the UTC date, not a rolling 24 hours. + +During the trial, full responses carry trial metadata so you can build countdown UX: ```json { @@ -81,55 +85,56 @@ Every athlete's first calculate request starts a **30-day trial of full-precisio } ``` -Over the daily cap, responses degrade to teaser ranges (never an error) and include `trial_cap_reached: true` plus a human-readable `trial_cap_note` you can surface directly to the athlete — it does the apology and the why for you. Batch scenarios debit the cap individually. After 30 days, responses are teaser tier until the athlete subscribes. +`trial_ends_at` is a Unix timestamp in milliseconds. + +Past the daily cap, responses fall back to teaser ranges rather than erroring, and carry `trial_cap_reached: true`, `trial_calls_remaining_today: 0`, and a `trial_cap_note` written for the athlete that you can surface as-is. Batch scenarios debit the cap one by one, so a 10-scenario batch spends 10. After the 30 days, responses stay teaser tier until the athlete subscribes. ## Subscription flow When an athlete wants full precision: -1. Your app shows teaser data with the upgrade CTA -2. Athlete taps the upgrade link (`subscribe_url` from the `subscription_cta`) — for athlete-scoped requests it carries a signed token (`pst`) identifying *which* athlete is upgrading -3. The athlete lands on Saturday's checkout page (partner-branded), pays via Stripe, and is sent back toward your app -4. Saturday writes the link and fires the `subscription.created` webhook — the athlete's next API call returns full precision data +1. Your app shows teaser data with the upgrade CTA. +2. The athlete taps `subscribe_url` from the `subscription_cta`. On athlete-scoped requests that URL carries a signed token (`pst`) identifying which athlete is upgrading. +3. They land on Saturday's partner-branded checkout page, pay via Stripe, and are sent back toward your app. +4. Saturday writes the link and fires the `subscription.created` webhook. The athlete's next API call returns full precision. -You don't handle payment — Saturday manages the subscription. Two important details: +Saturday manages the subscription, so you handle no payment. Two details decide whether the loop closes: -- **The unlock requires athlete-scoped requests.** Only CTAs minted from requests that included an `athlete_id` carry the `pst`; identity-less CTAs are attribution-only and can't auto-unlock anyone. -- **Already-subscribed Saturday users are never double-charged.** If the athlete already has an active Saturday subscription, checkout links their account to your athlete instead of charging, and `subscription.created` fires with `source: "existing_subscription_linked"`. +- **The unlock requires athlete-scoped requests.** Only CTAs minted from a request that included an `athlete_id` carry the `pst`. An identity-less CTA is attribution only and cannot unlock anyone. +- **Already-subscribed Saturday users are not double-charged.** If the athlete already has an active Saturday subscription, checkout links their account to your athlete instead of charging, and `subscription.created` fires with `source: "existing_subscription_linked"`. ### Checking entitlement -`GET /v1/athletes/{id}` returns a computed `subscription_status` field — `full` | `trial` | `teaser` — for polling after checkout-return or support lookups. +`GET /v1/athletes/{id}` returns a computed `subscription_status` of `full`, `trial`, or `teaser`. Use it to poll after a checkout return, or for support lookups. Reading it never starts or debits the trial. ### Return-to-app handoff -Give Saturday a `return_url` (https or deep link) at partner onboarding and the post-payment success page shows a "Back to your app" button — athletes land back in your product with full precision already flowing. +Register a `return_url` (https or a deep link) on your partner account and the post-payment success page shows a "Back to your app" button, so athletes land back in your product with full precision already flowing. ### Automatic linking by email -Some paying Saturday athletes never touch your subscribe CTA — they subscribed **inside the Saturday app** (Apple/Google in-app purchase) or on saturday.fit before joining your platform. Saturday links these automatically: when the `email` you set on an athlete exactly matches a Saturday account's email (case-insensitive; no fuzzy matching), the records are linked and the athlete's API responses unlock. +Some paying Saturday athletes never touch your subscribe CTA. They subscribed inside the Saturday app through Apple or Google, or on saturday.fit, before they joined your platform. Saturday links these for you: when the `email` you set on an athlete matches a Saturday account's email, the records are linked and that athlete's API responses unlock. -- **Opt in by supplying emails.** Set `email` on your athletes (`POST`/`PATCH /v1/athletes`). Matching runs when you write an athlete email, when a Saturday subscription activates, and in a nightly sweep. -- **You get the same webhook.** When a match links an actively paying account, `subscription.created` fires with `source: "email_match"` — handle it exactly like a checkout unlock. -- **Ambiguity never auto-links.** Multiple athletes sharing an email, or a conflict with an existing link, goes to Saturday-side human review instead. -- **No revenue share on matched links.** These subscriptions weren't driven through your platform (many predate it), so they don't appear on your revenue statement. -- **Only a tier boolean is released.** The match tells your platform the athlete's subscription tier — never payment details, purchase history, or Saturday profile data. +- **Opt in by supplying emails.** Set `email` on your athletes via `POST` or `PATCH /v1/athletes`. Matching runs when you write an athlete email, when a Saturday subscription activates, and in a nightly sweep. +- **Matching is exact.** Case and surrounding whitespace are ignored; nothing else is. There is no fuzzy matching. +- **You get the same webhook.** When a match links an actively paying account, `subscription.created` fires with `source: "email_match"`. Handle it exactly like a checkout unlock. +- **Ambiguity never auto-links.** Several athletes sharing an email, or a conflict with an existing link, goes to Saturday-side human review. +- **No revenue share on matched links.** These subscriptions were not driven through your platform, and many predate it, so they do not appear on your revenue statement. +- **Only the tier is released.** The match tells your platform the athlete's subscription tier, never payment details, purchase history, or Saturday profile data. -If you have a partner-negotiated offer with Saturday (e.g. member pricing for your annual subscribers), assert eligibility by setting `partner_plan: "annual"` on the athlete (`PATCH /v1/athletes/{id}`). Eligible athletes' CTAs carry the offer claim and checkout prices accordingly. You may advertise the offer anywhere; only asserted athletes can redeem it. +### Bundle offers + +If you have negotiated an offer with Saturday, such as member pricing for your annual subscribers, assert an athlete's eligibility by setting `partner_plan: "annual"` on them (`PATCH /v1/athletes/{id}`). Their CTA then carries the offer claim and checkout prices accordingly. You may advertise the offer anywhere; only asserted athletes can redeem it. ### Organization (team) offers -A coach or team on your platform can have a negotiated discount of their own. Record it on the organization (`PUT /v1/organizations/{org_id}/offer`) and assert each athlete's affiliation via `org_id` — see [Organizations → Organization offers](/guides/organizations#organization-offers-negotiated-discounts). +A coach or team on your platform can have a negotiated discount of their own. Record it on the organization (`PUT /v1/organizations/{org_id}/offer`) and assert each athlete's affiliation via `org_id`. See [Organizations, organization offers](/guides/organizations#organization-offers-negotiated-discounts). ### How discounts stack -When an athlete qualifies for more than one discount source (a partner offer **and** an org offer), the percents combine **multiplicatively** and the total is **hard-capped at 30%**: - -``` -20% partner + 15% org → 1 − (0.80 × 0.85) = 32% → capped to 30% -``` +When an athlete qualifies for more than one discount source, say a partner offer and an org offer, the percents combine multiplicatively and the total is capped at 30%. A 20% partner offer stacked with a 15% org offer gives `1 − (0.80 × 0.85)`, or 32%, which the cap brings back to 30%. -Checkout collapses the stack into a single combined discount, and Saturday's subscribe landing page displays the stacked percent plus which sources contributed — athletes always see the exact number they'll pay, never a per-source figure that checkout won't honor. +Checkout collapses the stack into one combined discount, and Saturday's subscribe landing page shows the stacked percent alongside which sources contributed. Athletes see the number they will pay, never a per-source figure checkout would not honor. ## Testing the loop (test environment) @@ -149,26 +154,15 @@ Your test base URL comes with your `sk_test_` key at onboarding. You can also run a real test checkout with Stripe's `4242 4242 4242 4242` card from the CTA link in any test-env teaser response. - **iOS partners:** opening Saturday's web checkout from inside your iOS app is an external purchase link for digital content — review Apple's current external-link entitlement rules for your app's situation. The CTA URL works in any browser context. + iOS partners: opening Saturday's web checkout from inside your iOS app is an external purchase link for digital content. Check Apple's current external-link entitlement rules against your app's situation. The CTA URL works in any browser context. ## Attribution -Teaser data must carry the "Powered by Saturday" mark, linked, near the numbers. Full data carries a -lighter mark wherever Saturday data appears. That linked mark is how the loop closes: athletes seeing -teasers find their way to full precision through it. +Teaser responses set `attribution.required` to `true`; full responses set it to `false`. Teaser data must carry the linked "Powered by Saturday" mark near the numbers, because that mark is the path an athlete takes from a range to their exact numbers. Full data carries a lighter mark wherever Saturday data appears. -Full placement rules, marks, sizing, and the per-surface matrix live on the -[Brand & Attribution](/guides/attribution) page. +Placement rules, marks, sizing, and the per-surface matrix are on the [Brand & Attribution](/guides/attribution) page. -## Partner value exchange +## Why the model works this way -- **Partners get**: Free nutrition intelligence for their platform -- **Saturday gets**: Distribution to athletes who may subscribe -- **Athletes get**: Personalized nutrition whether they subscribe or not - -The API is designed as a distribution play. More partners = more athletes seeing Saturday = more subscribers. The API doesn't need to make money directly — it needs to make Saturday the default nutrition layer. - - -**Trial clock (2026-06):** the 30-day full-precision trial starts at the athlete's first *narrower-than-full-wide* calculation — i.e., once any real profile data exists. Zero-data calculations never start (or burn) the trial. Collect the fueling profile first ([Athlete Onboarding](/guides/onboarding)) and the trial window delivers genuinely exact numbers from day one. - +Athletes see useful fueling whether or not they pay, so nothing about your integration is broken for a free user. Saturday reaches athletes it would otherwise never meet. You get nutrition intelligence in your product without building or maintaining a nutrition engine, and without carrying nutrition billing. diff --git a/guides/mcp-integration.mdx b/guides/mcp-integration.mdx index 7ba8fa7..f0df4a8 100644 --- a/guides/mcp-integration.mdx +++ b/guides/mcp-integration.mdx @@ -11,9 +11,7 @@ Saturday provides a native [Model Context Protocol (MCP)](https://modelcontextpr ## What is MCP? -MCP is an open standard that lets AI models discover and use external tools. Instead of writing custom API client code, an AI agent connects to Saturday's MCP server and automatically discovers available nutrition tools — their inputs, outputs, descriptions, and safety constraints. - -Think of it as USB for AI tools: plug in and it works. +MCP is an open standard that lets AI models discover and use external tools. Instead of writing custom API client code, an AI agent connects to Saturday's MCP server and discovers the available nutrition tools at runtime, along with their inputs, outputs, descriptions, and safety constraints. ## Why use Saturday via MCP? @@ -35,7 +33,7 @@ There are two ways to reach Saturday's MCP server, depending on who you are: | A **person** (athlete or coach) | "Sign in with Saturday" OAuth via the [Claude connector](/guides/coach-connector) | Your own data (athlete) and, for coaches, your roster + config | - **End users connect via the Claude connector, not an API key.** If you're an individual athlete or coach connecting your own Saturday account to Claude, see the [Claude Connector guide](/guides/coach-connector) — you paste `https://api.saturday.fit/mcp` into Claude and sign in; no API key needed. The configuration below is for **partners** integrating their platform with a partner API key. + **End users connect via the Claude connector, not an API key.** If you're an individual athlete or coach connecting your own Saturday account to Claude, see the [Claude Connector guide](/guides/coach-connector): you paste `https://api.saturday.fit/mcp` into Claude and sign in, with no API key. The configuration below is for **partners** integrating their platform with a partner API key. ### Server configuration (partners) @@ -49,7 +47,7 @@ Add Saturday to your MCP client configuration with your partner API key: "url": "https://api.saturday.fit/mcp", "transport": "streamable-http", "headers": { - "Authorization": "Bearer sk_test_abc123def456" + "Authorization": "Bearer YOUR_PARTNER_API_KEY" } } } @@ -66,7 +64,7 @@ When your agent connects, it discovers tools including: |------|-------------| | `calculate_nutrition` | Calculate fuel/hydration/electrolyte prescription for an activity | | `search_products` | Search the curated nutrition product database | -| `analyze_product_fit` | Score how a real product covers an athlete's prescription | +| `analyze_product_fit` | Score how well a product covers an athlete's prescription | | `get_athlete` / `update_athlete` | Read or update an athlete's profile and settings | | `create_activity` | Create an activity for an athlete | | `calculate_activity_prescription` | Calculate the prescription for an existing activity | @@ -76,14 +74,18 @@ When your agent connects, it discovers tools including: | `infer_activity_type` | Infer activity type from metadata | | `search_knowledge` | Search Saturday's sports nutrition knowledge base | -This is an illustrative slice; an athlete connection currently exposes ~20 tools and a coach connection more (see the coach note below). Each tool includes rich descriptions, structured input schemas, and tool annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`) that help AI agents use them correctly. +That is a slice, not the catalog. An athlete connection currently exposes about twenty tools, a coach connection more (see the coach note below), and the set moves as features ship, so call `tools/list` rather than hardcoding it. Each tool carries a description, a structured input schema, and MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`). + +Some tools are feature-gated and simply do not appear on connections that lack the feature, including the two bottling tools below. A tool you cannot see is also not callable, so absence from `tools/list` is the answer, not an error to retry. ### Bottle Builder (interactive MCP App) -`build_bottling_plan` is an **[MCP App](https://modelcontextprotocol.io/)** — alongside its structured result it returns an interactive HTML view (resource `ui://saturday/bottle-builder`) that renders inline in supporting clients (e.g. Claude.ai). The athlete can drag a strategy slider (even / balanced / concentrated), move fuel between bottles, adjust fill levels, edit their real vessels, and watch carbs/sodium/scoop amounts recompute live. When the target won't fit the bottles on hand, it returns an honest "carry it more concentrated, top up with water" plan rather than a dead end. Clients that don't render MCP App views still receive the full plan as text and structured content. +`build_bottling_plan` is an **[MCP App](https://modelcontextprotocol.io/)**: alongside its structured result it returns an interactive HTML view (resource `ui://saturday/bottle-builder`) that renders inline in supporting clients such as Claude.ai. The athlete can drag a strategy slider (even, balanced, or concentrated), move fuel between bottles, adjust fill levels, edit their own vessels, and watch carbs, sodium, and scoop amounts recompute live. When the target will not fit the bottles on hand, it returns a "carry it more concentrated, top up with water" plan rather than a dead end. Clients that don't render MCP App views still receive the full plan as text and structured content. - **Coach tools.** When a **coach** (Business tier or higher) connects via the [Claude connector](/guides/coach-connector), an additional set of roster + configuration tools lights up — `get_roster`, `get_roster_digest`, `get_athlete_fueling_rollup`, `get_athlete_report`, `get_session_detail`, `set_notification_rules`, `apply_alert_preset`, `set_report_settings`, `register_webhook`, and more. These are invisible to athlete-only users and to other partners. See the [Claude Connector guide](/guides/coach-connector) for the full coach tool catalog and the [Coach API](/guides/coach-api) for the equivalent REST surface. + **Coach tools.** When a **coach on Pro Coach tier or higher** (Pro Coach, Head Coach, Business, or Enterprise) connects via the [Claude connector](/guides/coach-connector), an additional set of roster and configuration tools appears: `get_roster`, `get_roster_digest`, `get_athlete_fueling_rollup`, `get_athlete_report`, `get_session_detail`, `get_notification_rules`, `set_notification_rules`, `apply_alert_preset`, `get_report_settings`, `set_report_settings`, `list_webhooks`, `register_webhook`, and `delete_webhook`. These are invisible to athlete-only users and to other partners, and tier is re-checked per request, so a lapsed coach loses them without an error. See the [Claude Connector guide](/guides/coach-connector) for the full coach tool catalog and the [Coach API](/guides/coach-api) for the equivalent REST surface. + + The webhook tools are the one place where being able to call a tool does not mean it will do anything. Registration succeeds on any coach tier and hands back a signing secret and an endpoint marked active, but concern events are only delivered to coaches on **Business or Enterprise**. On Pro Coach or Head Coach the endpoint stays quiet, with nothing on the endpoint object to say why. ## Example: Claude agent with Saturday MCP @@ -100,22 +102,15 @@ Here's how a Claude-powered agent might use Saturday's tools in a conversation: ## Safety-aware tool descriptions -Saturday's MCP tools include safety metadata in their descriptions. This ensures AI agents know that: +Tool descriptions carry their safety contract inline, so a connecting agent reads it as part of discovery rather than needing this page. `calculate_nutrition` opens "Calculate personalized fuel, hydration, and electrolyte targets for an endurance activity", then states what the result contains, when to call it, and how to report it, including which internal tuning keys not to echo back to the athlete. -- Prescriptions are **guidance for human consideration**, not automated commands -- Safety warnings must be surfaced to the user -- The `not_instructions: true` field means "present this to the human, don't execute it" +Three constraints run through them: -Example tool description (what the agent sees): +- Prescriptions are **guidance for human consideration**, not automated commands. +- Safety warnings must be surfaced to the user. +- The `not_instructions: true` field on a result means "present this to the human, do not execute it". -``` -Tool: calculate_nutrition -Description: Calculate a personalized fuel, hydration, and electrolyte -prescription for an endurance activity. Returns safety metadata including -risk level, warnings, and guardrails. IMPORTANT: Results are nutrition -guidance for human review — not executable instructions. Always present -safety warnings to the user. -``` +Read the live descriptions from `tools/list` rather than copying them into your own prompt: they change, and the copy in your prompt will not. ## Tool call example @@ -129,7 +124,7 @@ async with ClientSession( StdioServerParameters( command="npx", args=["mcp-remote", "https://api.saturday.fit/mcp"], - env={"SATURDAY_API_KEY": "sk_test_abc123def456"}, + env={"SATURDAY_API_KEY": os.environ["SATURDAY_API_KEY"]}, ) ) as session: # List available tools @@ -162,7 +157,7 @@ const transport = new StreamableHTTPClientTransport( { requestInit: { headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, }, }, } @@ -197,16 +192,16 @@ When building AI agents that consume Saturday via MCP: - **Surface all safety warnings** to the human user - **Present prescriptions as recommendations**, not commands - **Include "Powered by Saturday"** attribution for teaser-tier responses -- **Cache results** when inputs haven't changed — prescriptions are deterministic -- **Handle errors gracefully** — if a tool call fails, explain why to the user +- **Cache results** when inputs haven't changed. Prescriptions are deterministic +- **Handle errors gracefully.** If a tool call fails, explain why to the user ### Don't - **Don't autonomously act on prescriptions** (e.g., auto-ordering supplements) - **Don't strip safety metadata** from results before presenting to users - **Don't modify prescription numbers** based on your own logic -- **Don't use response data for ML training** — this violates Saturday's data policy -- **Don't make excessive tool calls** — batch when possible +- **Don't use response data for ML training.** This violates Saturday's data policy +- **Don't make excessive tool calls.** Batch when possible ## LLM discoverability @@ -214,7 +209,7 @@ Saturday publishes machine-readable context files for AI agents: | File | URL | Purpose | |------|-----|---------| -| `llms.txt` | `https://api.saturday.fit/llms.txt` | Brief API overview for LLM context | -| `llms-full.txt` | `https://api.saturday.fit/llms-full.txt` | Complete API documentation for LLM context | +| `llms.txt` | `https://docs.saturday.fit/llms.txt` | API overview for LLM context | +| `llms-full.txt` | `https://docs.saturday.fit/llms-full.txt` | Full API documentation as a single file | -These files follow the [llms.txt standard](https://llmstxt.org/) and provide structured context that helps AI agents understand Saturday's capabilities without reading the full documentation site. +These follow the [llms.txt standard](https://llmstxt.org/) and give an agent Saturday's API surface without crawling the documentation site. `https://saturday.fit/llms.txt` serves the same pair for the consumer product rather than the API. diff --git a/guides/nutrition-calculation.mdx b/guides/nutrition-calculation.mdx index 4bad94c..2fcb40e 100644 --- a/guides/nutrition-calculation.mdx +++ b/guides/nutrition-calculation.mdx @@ -1,13 +1,13 @@ --- title: 'Nutrition Calculation' -description: 'The core of Saturday — personalized fuel, hydration, and electrolyte prescriptions' +description: 'Personalized carbohydrate, sodium, and fluid prescriptions' sidebarTitle: 'Nutrition Calculation' icon: 'flask' --- # Nutrition Calculation -The nutrition calculation endpoint is Saturday's core product. It takes an activity description and returns a personalized fuel prescription — carbohydrate, hydration, and electrolyte targets — with safety guardrails applied. +This endpoint takes an activity description and returns a fuel prescription: carbohydrate, sodium, and fluid targets, with safety guardrails applied. ```bash POST /v1/nutrition/calculate @@ -15,7 +15,7 @@ POST /v1/nutrition/calculate ## Progressive enrichment -Saturday works with whatever data you have. More data produces more accurate prescriptions — but even minimal inputs return useful results. +Saturday works with whatever data you have. More data narrows the prescription; minimal inputs still return a usable result. ### Minimal inputs @@ -24,9 +24,12 @@ The bare minimum for a calculation: ```python Python +import os +import requests + response = requests.post( "https://api.saturday.fit/v1/nutrition/calculate", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "activity_type": "run", "duration_min": 90, @@ -41,7 +44,7 @@ const response = await fetch( { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -59,7 +62,7 @@ This returns a prescription with a low `confidence_score`. The numbers are based ### Standard inputs -Add intensity and environmental conditions for a meaningfully better prescription: +Add intensity and environmental conditions: ```json { @@ -71,7 +74,7 @@ Add intensity and environmental conditions for a meaningfully better prescriptio } ``` -Thermal stress dramatically affects hydration and sodium needs. A 3-hour ride at high thermal stress requires very different fueling than the same ride in cool conditions. +Thermal stress drives the fluid and sodium targets. A 3-hour ride at high thermal stress fuels differently from the same ride in cool conditions. ### Comprehensive inputs @@ -88,11 +91,7 @@ For the highest accuracy, include an athlete reference: } ``` -When you include an `athlete_id`, Saturday pulls their stored profile — sweat level, saltiness, carb experience, fitness level, and past activity feedback. This is where prescriptions become genuinely personalized. - - - **The enrichment path:** Start with minimal inputs to get integrated fast, then progressively add more data as your integration matures. Every additional field improves accuracy. - +When you include an `athlete_id`, Saturday reads that athlete's stored profile: sex, year of birth, body weight, sweat level, saltiness, carb experience, usual carb consumption, satiety level, fitness level, and fueling concerns. Inline fields in the request body override the stored values for that one call. ## Understanding the response @@ -110,9 +109,7 @@ When you include an `athlete_id`, Saturday pulls their stored profile — sweat "max_safe_sodium_mg_per_hr": 3000, "confidence_score": 0.85, "requires_human_review": false, - "warnings": [ - "High thermal stress. Consider ice, cold fluids, and shade." - ], + "warnings": [], "not_instructions": true }, "attribution": { @@ -126,33 +123,33 @@ When you include an `athlete_id`, Saturday pulls their stored profile — sweat ### Confidence score -The `safety.confidence_score` (0.0-1.0) indicates how personalized the prescription is: +The `safety.confidence_score` (0.0-1.0) is derived from how wide the prescription band is: a complete fueling profile scores 1.0, and every unanswered field widens the band and lowers the score. `precision.missing_fields` tells you which answers would raise it. -| Range | Meaning | Typical inputs | -|-------|---------|----------------| -| 0.8-1.0 | Strong personalization, athlete history | Athlete profile + conditions + feedback | -| 0.5-0.8 | Good estimates with some personalization | Weight + intensity + thermal stress | -| 0.0-0.5 | Population-level defaults applied | Minimal inputs (type + duration + weight) | +| Range | Meaning | +|-------|---------| +| 0.8-1.0 | Complete or near-complete profile; the numbers are exact or close to it | +| 0.5-0.8 | Several profile fields still defaulted | +| 0.0-0.5 | Mostly population defaults; the band is at or near its cap | ## Comparing across conditions -To compare fueling across different conditions (e.g. a cool morning vs. a hot afternoon), call `POST /v1/nutrition/calculate` once per scenario and diff the results yourself, or use `POST /v1/nutrition/calculate/batch` to submit up to 50 scenarios in a single request. +To compare fueling across different conditions (a cool morning against a hot afternoon, say), call `POST /v1/nutrition/calculate` once per scenario and diff the results yourself, or use `POST /v1/nutrition/calculate/batch` to submit up to 50 scenarios in a single request. Each scenario in a batch debits your quota individually. - **Two prescription shapes.** The `calculate` endpoints return a **flat** response (`carb_g_per_hr`, `sodium_mg_per_hr`, … with a nested `safety` block). The stored-prescription read (`GET /v1/athletes/{id}/activities/{id}/prescription`) returns a **wrapped** response: `{ "prescription": { … }, "safety": { … } }`. Read the fields accordingly depending on which endpoint you called. + Two prescription shapes. The `calculate` endpoints return a flat response (`carb_g_per_hr`, `sodium_mg_per_hr`, … with a nested `safety` block). The stored-prescription read (`GET /v1/athletes/{id}/activities/{id}/prescription`) returns a wrapped response: `{ "prescription": { … }, "safety": { … } }`. Read the fields accordingly depending on which endpoint you called. ## Calculation timing -Running the calculator is a deliberate computation window, not an instant lookup — the same processing experience athletes see in the Saturday app: +Every engine run carries a deliberate computation window, the same one athletes see in the Saturday app. Size your loading state around it: -- **Single calculation** (`POST /v1/nutrition/calculate`, `POST .../activities/{id}/calculate`): expect roughly **1–3 seconds**, scaling gently with activity duration. Recalculating an activity that already has a prescription takes about half that. -- **Batch** (`POST /v1/nutrition/calculate/batch`): each scenario adds roughly half a single calculation's time. The response headers arrive immediately with `X-Batch-Estimated-Ms` — the expected total processing time — so you can size progress indicators before the body lands. The completed body includes `estimated_ms` and `elapsed_ms`. -- **Estimate without calculating:** send the same batch payload with `"estimate_only": true` to get `estimated_ms` back instantly, with no calculations run. Useful when your HTTP client can't read streamed headers early. -- **Reads are instant.** Fetching a stored prescription (`GET .../prescription`) never carries a computation window. +- **Single calculation** (`POST /v1/nutrition/calculate`, `POST .../activities/{id}/calculate`): roughly 1 to 3 seconds, scaling with activity duration and rising with intensity. Recalculating an activity that already has a prescription takes about half as long. +- **Batch** (`POST /v1/nutrition/calculate/batch`): each scenario adds about half a single calculation's time. The response headers arrive immediately with `X-Batch-Estimated-Ms`, the expected total processing time, so you can size a progress indicator before the body lands. The completed body includes `estimated_ms` and `elapsed_ms`. +- **Estimate without calculating:** send the same batch payload with `"estimate_only": true` to get `estimated_ms` back immediately, with no calculations run and no quota debited. Useful when your HTTP client can't read streamed headers early. +- **Reads are instant.** Fetching a stored prescription (`GET .../prescription`) carries no computation window. - **Show a progress indicator during calculation.** Athletes trust a prescription more when they see it being computed. Use the calculation window to display a "calculating fueling plan…" state rather than a spinner-free freeze. + Fill the window with a specific state, such as "calculating your fueling plan", rather than a bare spinner or a frozen screen. ## Teaser vs. full responses @@ -160,18 +157,18 @@ Running the calculator is a deliberate computation window, not an instant lookup Responses depend on the athlete's Saturday subscription status: - **Subscribed athletes** get exact numbers: `"carb_g_per_hr": 72` -- **Free/teaser athletes** get ranges: `"carb_range_g_per_hr": "55-85"` +- **Free/teaser athletes** get ranges: `"carb_range_g_per_hr": "60-90"` + +Teaser ranges are bucketed to a fixed grid, 30 g/hr for carbohydrate and 500 units/hr for sodium and fluid, so the same underlying number always lands in the same bucket. See [Freemium Model](/guides/freemium-model) for implementation details. ## Safety -Every calculation response includes a `safety` block. This is non-negotiable — Saturday is a nutrition API for athletes, and bad recommendations can cause real harm. - -See [Safety](/guides/safety) for the full guide on safety fields, risk levels, and display requirements. +Every calculation response includes a `safety` block, on every tier. See [Safety](/guides/safety) for the fields, the guardrails behind them, and what you are required to display. - **Safety data is never gated behind subscription status.** Even teaser responses include complete safety metadata. + Safety data is never gated behind subscription status. Teaser responses carry the same safety block as full ones. ## Supported activity types @@ -186,8 +183,8 @@ See [Safety](/guides/safety) for the full guide on safety fields, risk levels, a | Hiking | `hike` | Long-duration, variable intensity | | Weightlifting | `lift` | Strength training | -Use `GET /v1/activity-types` for the complete list with descriptions. +These seven keys are the complete set. `GET /v1/activity-types` returns the same list with descriptions, and accepts an optional `?activity_type=` filter to validate a single key. -**Graduated precision (2026-06):** every calculation response now carries a `precision` object — `profile_complete`, impact-sorted `missing_fields`, and an onboarding invite URL. Exact numbers require a complete fueling profile; incomplete profiles get honest bands (never wider than teaser ranges). See [Athlete Onboarding](/guides/onboarding). +Every calculation response carries a `precision` object: `profile_complete`, impact-sorted `missing_fields`, and an onboarding invite. Exact numbers require a complete fueling profile; an incomplete one gets a band instead. See [Athlete Onboarding](/guides/onboarding). diff --git a/guides/oauth2.mdx b/guides/oauth2.mdx index 9213859..eacdf13 100644 --- a/guides/oauth2.mdx +++ b/guides/oauth2.mdx @@ -7,23 +7,23 @@ icon: 'lock' # OAuth2 -OAuth2 lets athletes connect their existing Saturday accounts to your platform. Instead of creating new partner-scoped athletes, you can request access to an athlete's real Saturday profile — with their explicit consent. +OAuth2 lets athletes connect their existing Saturday accounts to your platform. Instead of creating new partner-scoped athletes, you request access to an athlete's own Saturday profile, with their explicit consent. ## When to use OAuth2 | Scenario | Auth method | |----------|-------------| -| You manage athlete profiles in your platform | **API Key** — create athletes via the API | -| Athletes already have Saturday accounts and want to connect | **OAuth2** — athlete grants you access | -| You want access to an athlete's full Saturday history | **OAuth2** — requires athlete consent | +| You manage athlete profiles in your platform | **API Key**, create athletes via the API | +| Athletes already have Saturday accounts and want to connect | **OAuth2**, athlete grants you access | +| You want access to an athlete's full Saturday history | **OAuth2**, requires athlete consent | Most partners start with API keys. Add OAuth2 when athletes tell you they already have Saturday accounts. -## The flow + + **The athlete must hold an active Saturday subscription.** The authorize endpoint checks entitlement before the consent screen renders, and an athlete without an active subscription or coach tier is shown a subscribe page instead of your consent prompt. No account is created and no authorization code is issued. Entitlement is re-checked on every refresh, so a lapsed subscription ends your access within about an hour, when the current access token expires. Resubscribing inside the refresh token's 90-day window reconnects on the next refresh with no re-consent. + -``` -Your app → Saturday authorize → Athlete logs in → Consent → Code → Token exchange → API access -``` +## The flow @@ -64,7 +64,7 @@ code_verifier = secrets.token_urlsafe(32) digest = hashlib.sha256(code_verifier.encode("utf-8")).digest() code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("utf-8") -# Store code_verifier in your session — you need it for token exchange +# Store code_verifier in your session: you need it for token exchange print(f"Verifier: {code_verifier}") print(f"Challenge: {code_challenge}") ``` @@ -81,7 +81,7 @@ const codeChallenge = crypto .update(codeVerifier) .digest("base64url"); -// Store codeVerifier in your session — you need it for token exchange +// Store codeVerifier in your session: you need it for token exchange console.log(`Verifier: ${codeVerifier}`); console.log(`Challenge: ${codeChallenge}`); ``` @@ -108,10 +108,11 @@ https://api.saturday.fit/v1/oauth/authorize? | `client_id` | Yes | Your partner ID, or your [Client ID Metadata Document URL](#client-registration) | | `redirect_uri` | Yes | Must match a registered redirect URI | | `response_type` | Yes | Always `code` | -| `scope` | Yes | Space-separated list of requested scopes | -| `state` | Yes | Random string for CSRF protection | +| `scope` | No | Space-separated list of requested scopes. Omitting it grants `athlete:read` only | +| `state` | Recommended | Random string for CSRF protection. Saturday does not require it, but echoes whatever you send back on the callback, so send one | | `code_challenge` | Yes | PKCE challenge | | `code_challenge_method` | Yes | Always `S256` | +| `resource` | Recommended | Canonical URI of the resource the token will be used against, see [Resource indicators](#resource-indicators-rfc-8707) | ## Step 3: Handle the callback @@ -121,7 +122,7 @@ After the athlete consents, Saturday redirects to your `redirect_uri` with a cod https://your-app.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token ``` -**Always verify the `state` parameter matches what you sent.** This prevents CSRF attacks. +Verify that the `state` parameter matches what you sent before you exchange the code. Saturday echoes `state` back but does not validate it for you, so this check is yours to make; it is what prevents a CSRF attack from injecting an attacker's authorization code into your user's session. If the athlete denies access: ``` @@ -185,7 +186,7 @@ curl -H "Authorization: Bearer ACCESS_TOKEN_JWT" \ -d '{"activity_type": "bike", "duration_min": 120, "intensity_level": 3}' ``` -The API automatically scopes requests to the athlete who granted consent. You don't need to pass an `athlete_id` — it's in the JWT. +The API scopes requests to the athlete who granted consent. You do not pass an `athlete_id`; it is in the JWT, alongside the partner ID and the granted scopes. ## Step 6: Refresh expired tokens @@ -204,7 +205,7 @@ response = requests.post( ) new_tokens = response.json() -# Old refresh token is now invalid — save the new one +# Rotation: save the new refresh token, the old one stops working shortly access_token = new_tokens["access_token"] refresh_token = new_tokens["refresh_token"] ``` @@ -221,13 +222,13 @@ const response = await fetch("https://api.saturday.fit/v1/oauth/token", { }); const newTokens = await response.json(); -// Old refresh token is now invalid — save the new one +// Rotation: save the new refresh token, the old one stops working shortly ``` - **Refresh tokens are single-use.** Each refresh rotates both the access and refresh token. The old refresh token is immediately invalidated. Always store the new refresh token. + **Refresh tokens rotate.** Each refresh issues a new access and refresh token pair, and you must store the new refresh token. The token you just spent stays usable for a 10-minute grace window, so a retried or concurrent refresh re-issues a fresh pair instead of logging the athlete out. Past that window it is rejected and the athlete has to authorize again. Rotation is still single-use outside the grace window, which is what makes a stolen-and-replayed refresh token detectable. ## Available scopes @@ -240,10 +241,17 @@ const newTokens = await response.json(); | `activity:write` | Create, update, delete activities | | `nutrition:read` | Calculate nutrition prescriptions | | `ai:chat` | AI Coach conversations | +| `offline_access` | Be issued a refresh token, and use it | + +`offline_access` grants no data access of its own. It is the OIDC and MCP signal that you want to stay connected without sending the athlete back through consent every hour. Request it alongside your data scopes if you need refresh, and note that it is not included in the default `athlete:read` fallback. + +`ai:chat` is available to pre-registered clients only. It is deliberately excluded from the scopes advertised in discovery metadata, so a client registering through a Client ID Metadata Document cannot request it. ### Coach scopes -A coach (Business tier or higher) can additionally request coach scopes. These unlock the [Coach API](/guides/coach-api) and the coach tools in the [Claude connector](/guides/coach-connector). A coach's OAuth token also carries the athlete-self facet — the same token reads the coach's own athlete data on `/v1/athletes/*` and their roster on `/v1/coach/*`; the route decides which applies. +A coach on **Pro Coach tier or higher** (Pro Coach, Head Coach, Business, or Enterprise) can additionally request coach scopes. These unlock the [Coach API](/guides/coach-api) and the coach tools in the [Claude connector](/guides/coach-connector). A coach's OAuth token also carries the athlete-self facet: the same token reads the coach's own athlete data on `/v1/athletes/*` and their roster on `/v1/coach/*`, and the route decides which applies. + +Tier is re-checked on every request. If a coach's tier lapses, the coach scopes stop resolving and the token quietly degrades to athlete-self access rather than erroring, so handle a suddenly empty roster as a billing state, not an outage. | Scope | Access granted | |-------|---------------| @@ -262,7 +270,7 @@ Saturday supports two registration mechanisms: | Mechanism | When to use | How | |-----------|------------|-----| -| **Client ID Metadata Documents (CIMD)** | MCP hosts and any client that controls an HTTPS origin — **no registration step at all** | Use the URL of your hosted metadata document as your `client_id` | +| **Client ID Metadata Documents (CIMD)** | MCP hosts and any client that controls an HTTPS origin, with no registration step at all | Use the URL of your hosted metadata document as your `client_id` | | **Pre-registered clients** | Server-to-server partners with a standing relationship | Contact [api-support@saturday.fit](mailto:api-support@saturday.fit) for a `client_id` | ### Client ID Metadata Documents @@ -288,11 +296,11 @@ Saturday fetches, validates, and caches the document when it first sees your `cl - CIMD clients are **public clients**: `token_endpoint_auth_method` must be `none` (or absent), and PKCE is mandatory. Confidential CIMD clients (`private_key_jwt`) are not supported. - Documents are cached per their `Cache-Control` headers (clamped between 5 minutes and 24 hours), so metadata changes propagate within that window. -Saturday advertises support via `"client_id_metadata_document_supported": true` in its Authorization Server Metadata — spec-compliant MCP clients (including Claude) select CIMD automatically. +Saturday advertises support via `"client_id_metadata_document_supported": true` in its Authorization Server Metadata, so spec-compliant MCP clients, including Claude, select CIMD automatically. ## Discovery (remote MCP connectors) -For the [Claude connector](/guides/coach-connector), clients auto-discover the authorization server — no manual configuration. Saturday serves the standard metadata documents (derived per-environment from the request host): +For the [Claude connector](/guides/coach-connector), clients auto-discover the authorization server, with no manual configuration. Saturday serves the standard metadata documents, derived per-environment from the request host: | Document | Path | Spec | |----------|------|------| @@ -303,13 +311,13 @@ An unauthenticated request to `/mcp` returns `401` with a `WWW-Authenticate: Bea ## Resource indicators (RFC 8707) -Saturday implements [RFC 8707 Resource Indicators](https://www.rfc-editor.org/rfc/rfc8707.html) — token audience binding, as required by the MCP authorization spec. A client names the resource it intends to use the token with via the `resource` parameter on the authorize **and** token requests, using the **canonical MCP server URI**: +Saturday implements [RFC 8707 Resource Indicators](https://www.rfc-editor.org/rfc/rfc8707.html), the token audience binding required by the MCP authorization spec. A client names the resource it intends to use the token with via the `resource` parameter on the authorize **and** token requests, using the **canonical MCP server URI**: ``` resource=https://api.saturday.fit/mcp ``` -When a `resource` is supplied, Saturday binds it into the access token's `aud` claim, and the MCP endpoint rejects any token whose audience was issued for a different resource. A token minted for Saturday can only be used against Saturday — it cannot be replayed elsewhere. +When a `resource` is supplied, Saturday binds it into the access token's `aud` claim, and the MCP endpoint rejects any token whose audience was issued for a different resource. A token minted for Saturday can only be used against Saturday; it cannot be replayed against another server. Tokens minted before audience binding shipped, and partner keys that never traverse this flow, carry no audience and are accepted as before. | Field | Required | Description | |-------|----------|-------------| @@ -319,26 +327,41 @@ A `resource` that is malformed (not an absolute URI, or carries a fragment) or n ## Error scenarios -| Scenario | Response | -|----------|----------| -| Invalid `client_id` | 400 + "Unknown client_id" | -| Mismatched `redirect_uri` | 400 + "redirect_uri does not match" | -| Missing PKCE | 400 + "PKCE is required" | -| Expired auth code (>10 min) | 400 + "Authorization code has expired" | -| Reused auth code | 400 + all tokens revoked | -| Wrong `code_verifier` | 400 + "PKCE verification failed" | -| Invalid / foreign `resource` | 400 + `invalid_target` | -| Expired access token | 401 + "access token has expired" | -| Token used against the wrong resource | 401 + "not issued for this resource" | -| Revoked refresh token | 400 + "refresh token has been revoked" | +Errors follow the RFC 6749 shape, `{"error": "...", "error_description": "..."}`. Branch on `error`; the description is for your logs and may change. + +| Scenario | Status | `error` | +|----------|--------|---------| +| Unknown `client_id`, or a metadata document that would not resolve | 400 | `invalid_request` | +| Mismatched `redirect_uri` | 400 | `invalid_request` | +| Missing `code_challenge`, or a method other than `S256` | 400 | `invalid_request` | +| `response_type` other than `code` | 400 | `unsupported_response_type` | +| Unrecognized scope, or a scope this client is not authorized for | 400 | `invalid_scope` | +| Expired auth code (older than 10 minutes) | 400 | `invalid_grant` | +| Reused auth code. Every token issued from it is revoked | 400 | `invalid_grant` | +| Wrong `code_verifier` | 400 | `invalid_grant` | +| Missing `code_verifier` when PKCE was used | 400 | `invalid_request` | +| Wrong `client_secret` on a confidential client | 401 | `invalid_client` | +| Grant type other than `authorization_code` or `refresh_token` | 400 | `unsupported_grant_type` | +| Refresh token revoked, expired, or reused past the rotation grace window | 400 | `invalid_grant` | +| Refresh attempted after the athlete's subscription lapsed | 400 | `invalid_grant` | +| Malformed `resource`, or one this server does not serve | 400 | `invalid_target` | +| `resource` on token exchange that does not match the authorized one | 400 | `invalid_target` | + +At the authorize endpoint an unknown `client_id` is `invalid_request`; at the token endpoint it is `invalid_client`. + +Access token failures are different in shape. A request carrying an expired token, a token for another resource, or a malformed token all return `401` in the standard API error envelope with type `authentication_error` and the single message `Invalid or expired access token.` The cause is deliberately not distinguished in the response, so treat any `401` on an API call as "refresh, then retry once, then re-authorize" rather than trying to parse which failure it was. + +The subscription-lapse case is worth handling distinctly: its description tells the athlete to resubscribe, and the refresh token is deliberately not consumed, so the same token works again once they do. ## Revoking tokens -Athletes can revoke access from their Saturday account settings. Partners can programmatically revoke: +Athletes can revoke access from their Saturday account settings. Partners can revoke programmatically: ```bash POST /v1/oauth/token/revoke Content-Type: application/x-www-form-urlencoded -token=REFRESH_TOKEN&client_id=your_partner_id +token=REFRESH_TOKEN ``` + +Per RFC 7009 this returns `200` whether or not the token existed, so a repeated revoke is not an error. diff --git a/guides/onboarding.mdx b/guides/onboarding.mdx index 683f5da..b0416e7 100644 --- a/guides/onboarding.mdx +++ b/guides/onboarding.mdx @@ -1,6 +1,6 @@ --- title: 'Athlete Onboarding' -description: 'Four ways to collect an athlete fueling profile — and why exact numbers require it' +description: 'Four ways to collect an athlete fueling profile, and why exact numbers require it' sidebarTitle: 'Athlete Onboarding' icon: 'clipboard-check' --- @@ -19,9 +19,10 @@ Calculation responses carry a `precision` object on every tier: "profile_complete": false, "missing_fields": [ { "field": "sweat_level", "required": true, + "display_label": "how much you sweat", "band_impact": { "carb_g_per_hr": 0.4, "sodium_mg_per_hr": 86.1, "fluid_ml_per_hr": 79.0 } } ], - "message": "Exact numbers unavailable: critical fields missing (sweat_level). Each answer narrows this band.", + "message": "For exact numbers, a few key details are still needed: how much you sweat. Each one narrows the range.", "onboarding": { "url": "https://saturday.fit/onboard?ot=...", "message": "Answer the missing questions once and this athlete gets exact numbers on every future call..." @@ -30,24 +31,34 @@ Calculation responses carry a `precision` object on every tier: } ``` -- **`profile_complete: true`** → the response carries exact numbers (`carb_g_per_hr` etc.). -- **`profile_complete: false`** → the response carries honest **bands** (`carb_range_g_per_hr` etc.) — never wider than free-tier teaser ranges, never falsely exact. Band width is measured from our engine's real sensitivity to each missing field, so `missing_fields` is sorted most-impactful-first: it is literally your collection roadmap. -- The 30-day trial clock starts at the athlete's **first narrower-than-full-wide calculation** — any real profile data starts it; zero-data calls never do. Collect the profile first and your athletes get 30 days of genuinely exact numbers. +- **`profile_complete: true`** puts exact numbers in the response (`carb_g_per_hr` and friends). +- **`profile_complete: false`** puts bands there instead (`carb_range_g_per_hr` and friends). Band width is measured by running the engine against each missing field at its plausible extremes, so `missing_fields` sorted most-impactful-first is your collection roadmap: the top entry is the question that buys the most precision. +- The trial clock starts at the athlete's first calculation carrying any real data. Zero-data calls never start it. Collect the profile first and the 30-day window delivers exact numbers rather than wide bands. See [Freemium Model](/guides/freemium-model#30-day-full-precision-trial). + +Each missing field carries a `display_label`, the plain-English name of the question ("how much you sweat"), so you can build the prompt without exposing an internal key. `precision.message` is written for an athlete to read and is safe to surface directly. `band_impact` is in per-hour units, and the numbers say how much narrower the band gets when that one field is answered. + +### What "complete" means, twice + +Two different completeness checks share a name, and mixing them up is the usual surprise. + +`precision.profile_complete`, on a calculation response, is per-call. It requires every profile field **and** the activity parameters for that call: `intensity_level`, `thermal_stress_level`, `meal_before_min`, and `is_race`. An athlete with a perfect stored profile still gets a band if the call omits those. On `POST /v1/nutrition/calculate` you send them in the request body; on `POST .../activities/{id}/calculate` they are read from the stored activity, so set them when you create it. + +The athlete's own `profile_complete` field, the one `?profile_complete=false` filters on and `athlete.profile_completed` fires for, covers the stored profile only: `sex`, `year_of_birth`, `weight_kg`, `sweat_level`, `saltiness`, `satiety_level`, `fitness_level`, `carb_experience`, `usual_carb_consumption`, and an answered concerns question. -The required (safety-core) fields are: `sex`, `year_of_birth`/`age`, `weight_kg`, `sweat_level`, `saltiness`, `carb_experience`, `usual_carb_consumption`, plus per-call `duration_min`, `intensity_level`, and `thermal_stress_level`. Recommended fields (`satiety_level`, `fitness_level`, `concerns`, `meal_before_min`, `is_race`) narrow bands further — exactness requires everything. +Of the profile fields, `sex`, `year_of_birth`, `weight_kg`, `sweat_level`, `saltiness`, `carb_experience`, and `usual_carb_consumption` are the safety core, and `missing_fields` marks them `required: true`. `satiety_level`, `fitness_level`, and `concerns` come back as `required: false` because each one moves the numbers less, but exactness needs all of them. ## Four ways to collect the profile ### 1. Hosted onboarding page (recommended start) -Every incomplete-profile response includes `precision.onboarding.url` — a durable, athlete-scoped link to Saturday's hosted onboarding page, co-branded with your platform. Send the athlete there (button, email, push — your call): +Every incomplete-profile response carries `precision.onboarding.url`, a durable athlete-scoped link to Saturday's hosted onboarding page, co-branded with your platform. Send the athlete there by button, email, or push, whichever fits your product. -- Asks **only the missing questions**, one per screen, ~2 minutes, mobile-first. -- Each answer commits immediately — a half-finished session still narrows bands. -- The finish screen asks the athlete's consent to show fuel for their most recent activity, then deep-links into **your** activity screen if you've registered an `activity_link_template` on your partner account (e.g. `yourapp://activity/{external_id}`) — otherwise it renders the numbers and returns via your `return_url`. -- The link stays valid: athletes can revisit to edit answers. +- It asks only the questions that are missing, one per screen, mobile-first, in about two minutes. +- Each answer commits as it is given, so a half-finished session still narrows the bands. +- The finish screen asks the athlete's consent to show fuel for their most recent activity. If you have registered an `activity_link_template` on your partner account (`yourapp://activity/{external_id}`, for instance), it deep-links into your activity screen. Otherwise it renders the numbers itself and returns the athlete via your `return_url`. +- The link stays valid, so athletes can come back and edit their answers. ### 2. Your UI, our questions (headless) @@ -55,17 +66,21 @@ Every incomplete-profile response includes `precision.onboarding.url` — a dura GET /v1/onboarding/questions ``` -Returns the versioned question schema — field names, types, the exact answer values Saturday stores (odd-point scales, not continuous sliders), copy, and which fields are required. Render natively in your app and write answers via [`PATCH /v1/athletes/{id}/settings`](/guides/athletes) or athlete create/update. **Attribution is required** when rendering our questions in your UI (the schema response carries the attribution object, same contract as calculations). +Returns the versioned question schema: field names, types, the answer values Saturday stores, the athlete-facing copy with its localization keys, and which fields are required. Render it natively and write answers through [`PATCH /v1/athletes/{id}/settings`](/guides/athletes#updating-the-fueling-profile) or athlete create and update. + +The answer values are odd-point selectors (1, 3, 5, 7, 9), not continuous sliders, and some questions map two labels onto one value: "Not sure" for saltiness stores the same 5 as "Somewhat salty". Key your option state on the label or the index, not on the value, or those options will collide. + +Attribution is required when you render Saturday's questions in your UI. The schema response carries the attribution object, the same contract as a calculation response. -Schema evolution promise: new questions always arrive as *recommended*, and athletes complete under a schema version keep exact numbers until they next edit their profile or 12 months pass — a schema change will never mass-downgrade your athletes. +New questions arrive as recommended rather than required, so a schema change does not turn a complete athlete incomplete overnight. `schema_version` ships in the response; record which version an athlete answered under so you can tell when re-asking is worthwhile. ### 3. Saturday app (once, forever) -If the athlete uses the Saturday app with the **same email address** they have on your platform, their full app onboarding silently powers their numbers on your platform too — live at calculation time, no sync. Their answers are used compute-only and are never exposed through the API; athletes can separately choose to share profile values with you from app settings. +If the athlete uses the Saturday app with the same email address they have on your platform, their app onboarding powers their numbers on your platform too, resolved at calculation time rather than synced. Those answers are used for computation only and are never exposed through the API. An athlete can separately choose to share their profile values with you from app settings. ### 4. Pass fields inline -Every calculate call accepts the full profile inline (`sweat_level`, `saltiness`, etc. — see [Nutrition Calculation](/guides/nutrition-calculation)). Inline values always win over stored ones. Good for stateless integrations; you carry the data. +Every calculate call accepts the full profile inline (`sweat_level`, `saltiness`, and the rest; see [Nutrition Calculation](/guides/nutrition-calculation)). Inline values win over stored ones for that call, and do not overwrite the stored profile. Good for stateless integrations, where you carry the data. ## Finding athletes to nudge @@ -73,4 +88,4 @@ Every calculate call accepts the full profile inline (`sweat_level`, `saltiness` GET /v1/athletes?profile_complete=false ``` -Lists athletes still short of exactness. Pair with the `athlete.profile_completed` [webhook](/guides/webhooks) — it fires exactly once when an athlete crosses into completeness, so you can celebrate in your UI the moment their numbers go exact. +Lists the athletes still short of exactness. Pair it with the `athlete.profile_completed` [webhook](/guides/webhooks), which fires once on the crossing into completeness, so you can mark the moment in your UI when an athlete's numbers go exact. diff --git a/guides/organizations.mdx b/guides/organizations.mdx index b9ba7c8..b760b1e 100644 --- a/guides/organizations.mdx +++ b/guides/organizations.mdx @@ -9,16 +9,19 @@ icon: 'building' Organizations let partners manage teams of athletes under a single billing structure. This is designed for coaching platforms, team managers, and enterprise partners who onboard athletes in groups. +The examples read your key from a `SATURDAY_API_KEY` environment variable, as in [Quickstart](/quickstart). Sandbox keys are issued with their own base URL; using one against `api.saturday.fit` returns `401 invalid_api_key`. + ## Creating an organization ```python Python +import os import requests response = requests.post( "https://api.saturday.fit/v1/organizations", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "display_name": "Seattle Running Club", "description": "Marathon training group", @@ -34,7 +37,7 @@ org_id = org["id"] const response = await fetch("https://api.saturday.fit/v1/organizations", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -60,16 +63,18 @@ The response includes server-set `id`, `created_at`, `updated_at`, and a maintai ## Managing members -Members are the organization's **people directory** (coaches and staff by email). They're separate from athlete records — linking a member row to an athlete via `athlete_id` is optional. +Members are the organization's **people directory**: coaches and staff, keyed by email. They are separate from athlete records, and linking a member row to an athlete via `athlete_id` is optional. ### Adding members ```python Python +import os + response = requests.post( f"https://api.saturday.fit/v1/organizations/{org_id}/members", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "email": "coach@seattlerunclub.com", "role": "admin", @@ -84,7 +89,7 @@ const response = await fetch( { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -103,7 +108,9 @@ const response = await fetch( | Role | Meaning | |------|---------| | `admin` | Organization manager (coach, team admin) | -| `member` | Everyone else | +| `member` | Everyone else. Applied when `role` is omitted | + +Any other value returns `400`. ### Listing and removing members @@ -112,7 +119,7 @@ GET /v1/organizations/{org_id}/members DELETE /v1/organizations/{org_id}/members/{member_id} ``` -Removing a member never deletes an athlete profile — it only removes the directory entry. +Removing a member never deletes an athlete profile; it removes the directory entry and decrements `member_count`. ## Seat subscriptions (team licensing) @@ -153,9 +160,9 @@ POST /v1/organizations/{org_id}/subscriptions/{sub_id}/licenses ## Organization offers (negotiated discounts) -A coach or team admin can negotiate a discount their athletes receive at **personal checkout** — "Team Alpine athletes get 15% off Saturday." You record it as the organization's offer; Saturday applies it automatically when an affiliated athlete follows a subscribe CTA from your app. +A coach or team admin can negotiate a discount their athletes receive at **personal checkout**, as in "Team Alpine athletes get 15% off Saturday." You record it as the organization's offer, and Saturday applies it automatically when an affiliated athlete follows a subscribe CTA from your app. -Each organization has at most **one active offer**. `PUT` creates or replaces it (previous offers are deactivated, never deleted — the document trail is the negotiation audit log). +Each organization has at most **one active offer**. `PUT` creates or replaces it. Previous offers are deactivated rather than deleted, so the document trail is the negotiation audit log. ### Setting the offer @@ -166,9 +173,11 @@ PUT /v1/organizations/{org_id}/offer ```python Python +import os + response = requests.put( f"https://api.saturday.fit/v1/organizations/{org_id}/offer", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "discount_percent": 15, "applies_to_plan": "12_month", @@ -184,7 +193,7 @@ const response = await fetch( { method: "PUT", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -201,11 +210,11 @@ const response = await fetch( | Field | Required | Description | |-------|----------|-------------| -| `discount_percent` | Yes | `1`–`30`. 30 is the stack cap (see below) — offers above the cap are rejected up front so you never promise athletes a number checkout won't honor. | +| `discount_percent` | Yes | `1` to `30`. 30 is the stack cap (see below), and offers above it are rejected up front so you never promise athletes a number checkout won't honor. | | `applies_to_plan` | No | `"12_month"`, `"1_month"`, or omit to apply to all plans | -| `negotiated_by` | No | Human label for the audit trail (coach name) — informational, not authentication | +| `negotiated_by` | No | Human label for the audit trail, such as a coach name. Informational, not authentication | | `starts_at` / `ends_at` | No | Unix **seconds**. Omit either for an open-ended window | -| `note` | No | Free text — why this discount exists | +| `note` | No | Free text: why this discount exists | The response echoes the stored offer plus `stack_cap_percent`, the hard ceiling on the total stacked discount (currently `30`). @@ -226,22 +235,19 @@ PATCH /v1/athletes/{athlete_id} ```json { - "org_id": "org_abc123" + "org_id": "5d8f3b21-7c04-4e19-9a6b-2f0c7e4d1a83" } ``` -`org_id` is partner-asserted (the same trust posture as `partner_plan`) and the organization must already exist. A seat license is **not** required — the discount is for the athlete's own purchase, not a license entitlement. Set `org_id` to an empty string to remove the affiliation. +`org_id` is partner-asserted, the same trust posture as `partner_plan`, and the organization must already exist: pointing at an org you have not created returns `400` rather than silently producing an athlete whose team discount never applies. A seat license is **not** required, since the discount is for the athlete's own purchase and not a license entitlement. Set `org_id` to an empty string to remove the affiliation. ### Stacking with partner offers If your platform also has a partner-level offer (e.g. bundle pricing for your annual subscribers), eligible athletes get **both**: the percents combine multiplicatively, and the total is hard-capped at **30%**. -``` -20% partner offer + 15% org offer -→ 1 − (0.80 × 0.85) = 32% → capped to 30% -``` +A 20% partner offer combined with a 15% org offer gives `1 - (0.80 × 0.85) = 32%`, which the cap reduces to 30%. -Checkout applies one combined discount, and Saturday's subscribe landing page shows the stacked percent — the exact number the athlete will pay — along with which sources contributed. See [Freemium Model → Bundle offers](/guides/freemium-model#bundle-offers) for the full discount flow. +Checkout applies one combined discount, and Saturday's subscribe landing page shows the stacked percent, the exact number the athlete will pay, along with which sources contributed. See [Freemium Model, Bundle offers](/guides/freemium-model#bundle-offers) for the full discount flow. ## Listing organizations @@ -266,4 +272,4 @@ PATCH /v1/organizations/{org_id} ## Finding an organization's athletes -Athletes reference their organization through the `org_id` field on the athlete record (`POST`/`PATCH /v1/athletes`, see [Organization offers](#organization-offers-negotiated-discounts) above). To build a team roster view, track your own athlete→org mapping when you assert `org_id`, or use the seat-subscription [license list](#assign-licenses-to-athletes) for licensed teams — `GET .../licenses` returns exactly the athletes the org covers. +Athletes reference their organization through the `org_id` field on the athlete record (`POST` and `PATCH /v1/athletes`, see [Organization offers](#organization-offers-negotiated-discounts) above). There is no endpoint that lists an organization's athletes. To build a team roster view, keep your own athlete-to-org mapping as you assert `org_id`, or for licensed teams use the seat-subscription [license list](#assign-licenses-to-athletes): `GET .../licenses` returns exactly the athletes the org covers. diff --git a/guides/safety.mdx b/guides/safety.mdx index f587555..738524d 100644 --- a/guides/safety.mdx +++ b/guides/safety.mdx @@ -1,16 +1,16 @@ --- title: 'Safety' -description: "Saturday's safety model — why it exists and how it works" +description: "Saturday's safety model: why it exists and how it works" sidebarTitle: 'Safety' icon: 'shield-halved' --- # Safety -Saturday is a nutrition API for endurance athletes. Bad nutrition advice can cause real harm — and in extreme cases, death. This page explains Saturday's safety model, why it exists, and what it means for your integration. +Saturday is a nutrition API for endurance athletes, and nutrition advice can cause harm, in rare cases fatal harm. This page covers Saturday's safety model, what the API gives you, and what you are expected to do with it. - **This is not a compliance formality.** Exercise-associated hyponatremia (EAH) has killed athletes at the Boston Marathon, London Marathon, Marine Corps Marathon, and multiple Ironman races. It happens when athletes overdrink and dilute their blood sodium to dangerous levels. Saturday's safety guardrails exist to prevent this. + Exercise-associated hyponatremia has killed athletes at the Boston Marathon, London Marathon, Marine Corps Marathon, and multiple Ironman races. It happens when athletes overdrink and dilute their blood sodium. Saturday's guardrails exist because of this failure mode, not to satisfy a compliance checklist. ## What can go wrong @@ -18,16 +18,18 @@ Saturday is a nutrition API for endurance athletes. Bad nutrition advice can cau | Condition | Cause | Consequence | |-----------|-------|-------------| | **Hyponatremia** | Overdrinking, insufficient sodium | Confusion, seizures, coma, death | -| **Heat stroke** | Under-hydrating in heat | Organ failure, death | -| **GI distress** | Too many carbs too fast | Vomiting, cramping, DNF | -| **Bonking** | Insufficient carbohydrate | Collapse, dangerous judgment impairment | +| **Heat illness** | Under-hydrating in heat | Organ failure, death | +| **GI distress** | Carbohydrate beyond trained gut tolerance | Vomiting, cramping, DNF | +| **Under-fueling** | Insufficient carbohydrate | Collapse, impaired judgment | | **Rhabdomyolysis** | Extreme exertion without fuel | Kidney failure | -Saturday's engine has physiological guardrails for all of these. Every prescription respects hard limits that the algorithm enforces regardless of inputs. +The two failure modes that dominate endurance are under-fueling and hyponatremia from drinking large volumes of plain water without sodium. Both are failures to take in enough of something. High carbohydrate and high sodium intakes are normal, and a long hot ride can correctly call for over 200 g of carbohydrate and over 3000 mg of sodium in total. If your UI adds caution on top of Saturday's numbers, it is likely to push athletes the wrong way. + +Saturday's engine applies limits on every calculation. They are score-based rather than a single fixed number: the athlete's profile and the activity's conditions set a range, and absolute ceilings bound it. ## The safety block -Every nutrition calculation response — including teasers and free responses — includes a `safety` object: +Every nutrition calculation response, teaser and full alike, includes a `safety` object: ```json { @@ -37,8 +39,7 @@ Every nutrition calculation response — including teasers and free responses "confidence_score": 0.72, "requires_human_review": false, "warnings": [ - "High thermal stress increases dehydration risk. Plan extra fluid access points.", - "Activities over 4 hours require careful sodium management. Monitor for signs of hyponatremia." + "Fluid intake exceeds recommended maximum. Consider reducing duration or consulting a sports dietitian." ], "not_instructions": true } @@ -46,125 +47,107 @@ Every nutrition calculation response — including teasers and free responses ``` - **Safety data is NEVER gated behind subscription status.** A teaser response may show carb ranges instead of exact numbers, but it will always show the full safety block with all warnings, confidence scores, and safe maximums. This is non-negotiable. + Safety data is never gated behind subscription status. A teaser response shows ranges instead of exact numbers, and carries the same safety block. -## Engine hard limits +### The two threshold fields -Saturday enforces absolute physiological boundaries that cannot be overridden by any input combination: +`max_safe_fluid_ml_per_hr` (1500) and `max_safe_sodium_mg_per_hr` (3000) are fixed advisory thresholds, the same on every response. They are the line above which Saturday flags a prescription, not the engine's absolute ceiling, and not a value derived from this athlete. Display them as guidance, and do not compute anything from them as though they were personalized. -| Guardrail | Exposed as | Why | -|-----------|-----------|-----| -| Fluid physiological limit | `max_safe_fluid_ml_per_hr` | Overdrinking causes hyponatremia | -| Sodium ceiling | `max_safe_sodium_mg_per_hr` | Excess sodium causes GI distress and nausea | -| Minimum sodium | (enforced internally) | Prevents hyponatremia in extended efforts | -| Carb gut tolerance | (enforced internally) | Exceeding gut capacity causes vomiting | -| Duration scaling | (enforced internally) | Steady-state assumptions break after 4+ hours | -| Eating disorder guardrails | (enforced internally) | Protects vulnerable athletes | +### Warnings -These guardrails are evaluated on every calculation. The `max_safe_fluid_ml_per_hr` and `max_safe_sodium_mg_per_hr` fields expose the hard ceilings so partners can display them. All other guardrails are enforced internally — prescriptions always respect them. +`warnings` is populated by comparing the prescription against those two thresholds. Two warnings exist: -## Confidence score and human review +| Condition | Warning | +|-----------|---------| +| Fluid above 1500 mL/hr | "Fluid intake exceeds recommended maximum. Consider reducing duration or consulting a sports dietitian." | +| Sodium above 3000 mg/hr | "Sodium intake is very high. Consult a sports dietitian for personalized guidance." | -The `confidence_score` (0.0-1.0) indicates how well-supported the prescription is. When `requires_human_review` is `true`, conditions are unusual enough that a human should verify the prescription before following it. +Most prescriptions cross neither threshold and come back with an empty `warnings` array. An empty array is the normal case, not a sign that safety checking was skipped: the engine's limits are applied to every calculation whether or not a warning results. -| Confidence range | Meaning | Your display recommendation | -|-----------------|---------|----------------------------| -| 0.8-1.0 | Strong personalization, well-understood conditions | No special display needed | -| 0.5-0.8 | Good estimates, some assumptions made | Show any warnings in your UI | -| 0.0-0.5 | Significant uncertainty or risk factors | Prominent warning, consider a confirmation step | +### Where each safety field is populated -## Dangerous vs. safe prescription example +The safety block is present on every prescription response, but not every endpoint fills all of it. -**Dangerous** (what a naive algorithm might produce for a 4-hour marathon in 30C heat): +| Field | `/v1/nutrition/calculate` and its batch | Activity calculate and prescription read | +|-------|---|---| +| `max_safe_fluid_ml_per_hr` | 1500 | 1500 | +| `max_safe_sodium_mg_per_hr` | 3000 | 3000 | +| `not_instructions` | `true` | `true` | +| `requires_human_review` | `false` | `false` | +| `confidence_score` | Derived from band width | Always `0` | +| `warnings` | Threshold warnings, else `[]` | Always `null` | -``` -Fluid: 1200 mL/hr -Sodium: 200 mg/hr -Carbs: 120 g/hr -``` + + If your integration drives fueling from `POST /v1/athletes/{id}/activities/{id}/calculate`, you are not receiving prescription warnings. Call `POST /v1/nutrition/calculate` with the same parameters when you need them, and handle `warnings: null` rather than assuming an array. + -This is dangerous because: -- 1200 mL/hr exceeds gastric emptying rate — athlete will drink it but not absorb it, diluting blood sodium -- 200 mg/hr sodium is far too low for hot conditions — hyponatremia risk is extreme -- 120 g/hr carbs without trained gut tolerance — GI distress guaranteed +## Engine limits -**Safe** (what Saturday produces for the same conditions): +Saturday bounds every calculation. The limits are computed per athlete and per activity, then clamped by absolutes that no input combination can exceed. -``` -fluid_ml_per_hr: 700 (capped by physiological limit) -sodium_mg_per_hr: 800 (elevated for salty sweater in heat) -carb_g_per_hr: 72 (limited by demonstrated tolerance) - -safety.max_safe_fluid_ml_per_hr: 1500 -safety.max_safe_sodium_mg_per_hr: 3000 -safety.confidence_score: 0.45 -safety.requires_human_review: true -safety.warnings: ["High thermal stress. Plan extra fluid access points and shade."] -safety.not_instructions: true -``` +| Guardrail | Absolute cap | How it narrows | +|-----------|--------------|----------------| +| Fluid | 1801 mL/hr | Thermal stress and intensity tighten the usable range well below the cap | +| Sodium | Body weight in pounds times 11, up to 5000 mg/hr | Carb experience, intensity, and thermal stress narrow the range | +| Sodium floor | Rises with thermal stress | Hot conditions cannot produce a low-sodium prescription | +| Carbohydrate | 150 g/hr | Dropped to 100 g/hr when the athlete is not prioritizing performance, then by `carb_upper_limit_override`, then by stated carb experience. An athlete who has never fueled above 30 g/hr is not handed a 120 g/hr target | +| Duration scaling | | Intake ranges are a function of duration, so a 6-hour target is not a 1-hour rate multiplied out | -The difference between these two prescriptions is potentially life-or-death. Saturday's guardrails ensure the second version is what athletes receive. +These apply on every calculation and are not partner-overridable. Note that the sodium cap is the one limit that scales with the athlete: a 70 kg athlete reaches it at roughly 1700 mg/hr, far below the 5000 mg/hr absolute. -## The `not_instructions` field + + Teaser ranges are display buckets, and both ends are bounded. The upper bound never exceeds the caps above: a 1700 mL/hr fluid prescription renders `1300-1800`, not `1500-2000`. The lower bound never reads zero for a value the engine actually prescribed, so a 350 mg/hr sodium prescription renders `200-500` rather than `0-500`. A zero low bound means the prescribed value is genuinely near zero. -For AI agents consuming Saturday's API (AI-to-AI communication), responses include: + The bounds still describe a bucket rather than a target. Render the range as a range, and do not treat either endpoint as a recommended intake. + -```json -{ - "not_instructions": true -} -``` +## Confidence score -This field tells AI consumers that Saturday's prescription data is **informational nutrition guidance, not executable instructions**. An AI agent should present this data to users for their consideration — not autonomously act on it (e.g., by automatically ordering supplements or modifying an athlete's plan without consent). +`confidence_score` (0.0-1.0) reports how much of the athlete's fueling profile was answered, derived from the width of the prescription band. A complete profile scores 1.0. It is a completeness signal, not a clinical risk score, and `precision.missing_fields` tells you which answers would raise it. See [Athlete Onboarding](/guides/onboarding). -## Display requirements for partners +| Confidence range | Meaning | Your display | +|-----------------|---------|--------------| +| 0.8-1.0 | Complete or near-complete profile | Show the numbers | +| 0.5-0.8 | Several fields still on defaults | Show the numbers, and prompt to finish the profile | +| 0.0-0.5 | Mostly defaults | Prefer the band over a single number, and prompt to finish the profile | -### Required + + `requires_human_review` is present on every response and is currently always `false`. Read it if you want to be forward-compatible, but do not build a flow whose only trigger is that field going `true`. + -- **Always show safety warnings** when warnings are present or `requires_human_review` is `true` -- **Never hide safety data** behind expandable sections or "advanced" toggles -- **Never strip safety metadata** from responses before displaying to users -- **Include the disclaimer** that prescriptions are guidance, not medical advice +## The `not_instructions` field -### Recommended +Every prescription response carries `not_instructions: true`. -- Show warnings **before or alongside** the prescription numbers, not hidden in details -- Use visual hierarchy (color, icons, positioning) to make warnings visible -- When `requires_human_review` is `true`, consider requiring user acknowledgment before proceeding +The field is aimed at AI consumers. It marks Saturday's prescription data as nutrition guidance for a person to consider, not a command to execute. An agent reading this API should present the numbers to its user rather than act on them by itself: no ordering supplements, no rewriting an athlete's plan, no triggering a purchase, without that person's explicit go-ahead. -### Prohibited +## Display requirements for partners -- Don't filter warnings based on your own risk assessment -- Don't apply your own safety logic on top of Saturday's — this creates conflicting advice -- Don't present prescriptions without any safety context -- Don't use Saturday's numbers as automated triggers (e.g., auto-ordering hydration packs) +### Required -## Contraindications +- Show safety warnings whenever `warnings` is non-empty. +- Do not hide safety data behind expandable sections or "advanced" toggles. +- Do not strip safety metadata from responses before displaying them. +- Include the disclaimer that prescriptions are guidance, not medical advice. -In rare cases, Saturday may flag a contraindication — a reason the calculation should be treated with extreme caution: +### Recommended -```json -{ - "contraindications": [ - { - "code": "extreme_heat_advisory", - "message": "Calculated conditions exceed safe exercise thresholds. Consult a physician before proceeding.", - "severity": "critical" - } - ] -} -``` +- Put warnings before or beside the prescription numbers, not in a details pane. +- Give warnings visual weight through color, icon, and position. +- Surface `precision.message` when the profile is incomplete, so an athlete understands why they are seeing a range. + +### Prohibited -Contraindications are rare and serious. If present, display them prominently and consider blocking the activity plan until the athlete explicitly acknowledges the risk. +- Do not filter warnings according to your own risk assessment. +- Do not layer your own safety logic on top of Saturday's. Two sets of limits produce conflicting advice, and yours will not know what the engine already capped. +- Do not present prescriptions with no safety context at all. +- Do not wire Saturday's numbers to automated triggers such as auto-ordering hydration products. ## Eating disorder sensitivity -When an athlete has the `eating_disorder_flag` set, Saturday: +Saturday's AI coach carries eating-disorder handling: it keeps to performance framing and avoids calorie, weight, and restriction language. -- Avoids calorie-focused language -- Removes weight/restriction framing from rationale text -- Applies conservative minimums (never under-fuels) -- Adjusts AI coaching conversation tone +The partner API has no athlete field for this. There is no `eating_disorder_flag` to set on an athlete, and the calculation engine takes no such input. If you hold that knowledge about an athlete, it stays on your side and shapes your own copy. -This flag is a backend safety signal. **Never expose it in partner UIs.** Partners set it based on their own knowledge of the athlete — Saturday uses it to protect that athlete across all interactions. +Note which direction the caution runs. Saturday's guardrails protect against under-fueling and against fluid without sodium. Adding restriction-flavored caution to a fueling target inverts that, so keep your framing on performance and on meeting the target. diff --git a/guides/webhooks.mdx b/guides/webhooks.mdx index 4cadee9..78e12f5 100644 --- a/guides/webhooks.mdx +++ b/guides/webhooks.mdx @@ -1,24 +1,27 @@ --- title: 'Webhooks' -description: 'Real-time event notifications with HMAC-SHA256 verification' +description: 'Event notifications signed with HMAC-SHA256' sidebarTitle: 'Webhooks' icon: 'bell' --- # Webhooks -Webhooks let Saturday push real-time event notifications to your server. Instead of polling the API for changes, register a webhook URL and Saturday will POST events to you as they happen. +Register a webhook URL and Saturday POSTs events to it as they happen, so you do not have to poll the API for changes. + +The examples read your key from a `SATURDAY_API_KEY` environment variable, as in [Quickstart](/quickstart). Sandbox keys are issued with their own base URL; using one against `api.saturday.fit` returns `401 invalid_api_key`. ## Registering a webhook ```python Python +import os import requests response = requests.post( "https://api.saturday.fit/v1/webhooks", - headers={"Authorization": "Bearer sk_test_abc123def456"}, + headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "url": "https://your-app.com/webhooks/saturday", "events": [ @@ -30,7 +33,7 @@ response = requests.post( ) webhook = response.json() -# Save webhook["secret"] — you need it to verify signatures +# Save webhook["secret"]: you need it to verify signatures print(f"Webhook secret: {webhook['secret']}") ``` @@ -38,7 +41,7 @@ print(f"Webhook secret: {webhook['secret']}") const response = await fetch("https://api.saturday.fit/v1/webhooks", { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -48,43 +51,51 @@ const response = await fetch("https://api.saturday.fit/v1/webhooks", { }); const webhook = await response.json(); -// Save webhook.secret — you need it to verify signatures +// Save webhook.secret: you need it to verify signatures console.log(`Webhook secret: ${webhook.secret}`); ``` - **Save the webhook secret immediately.** It is only returned once at creation time. You need it to verify that incoming webhooks are genuinely from Saturday. + **Save the webhook secret immediately.** It is returned once, at creation time, and never again. You need it to verify that incoming webhooks came from Saturday. +A partner may hold up to **20 webhooks**. Registration rejects URLs that resolve to private, loopback, or cloud-metadata addresses, and URLs on ports other than 443 and 80. The scheme must be `https` in production; the test environment also accepts `http`. If Saturday's key service is briefly unavailable at creation time the request returns `503` with `Retry-After` rather than storing an unencrypted secret, so retry on 503. + ## Available events +These event types are emitted today: + | Event | Triggered when | |-------|---------------| | `athlete.created` | A new athlete is created | | `athlete.updated` | An athlete profile is modified | +| `athlete.profile_completed` | An athlete's fueling profile crosses from incomplete to complete, which is the cue that this athlete is now eligible for exact prescriptions | | `athlete.deleted` | An athlete is deleted | | `activity.created` | A new activity is created | | `activity.updated` | An activity is modified | +| `activity.deleted` | An activity is deleted | | `prescription.calculated` | A nutrition prescription is generated | | `feedback.submitted` | Post-activity feedback is submitted | | `subscription.created` | An athlete unlocks full tier (paid checkout, linked existing subscription, or test-env simulation) | -| `subscription.updated` | An athlete's subscription changes | | `subscription.cancelled` | An athlete's access degrades to teaser (cancellation, expiry, or refund) | -| `conversation.message_sent` | An AI Coach message is sent | + +Registration also accepts four names that will not reach a partner webhook. `subscription.updated` and `partner.rate_limit_approaching` are reserved and have no emitter at all. `concern.detected` and `athlete.needs_attention` are live, but they are delivered on the separate coach webhook lane, to endpoints a coach registers against their own account, never to a partner webhook; see the [Coach API](/guides/coach-api). Subscribing to any of the four on a partner webhook is accepted and then silent, so do not build against them here. + +Any name outside this list is rejected with `400`, and one bad name fails the entire registration rather than the single event, so register only the names above. ### Subscription event payloads -`subscription.created` fires once per unlock — after the purchase record exists, never before. A resubscribe after cancellation fires it again. +`subscription.created` fires once per unlock, after the purchase record exists and never before. A resubscribe after cancellation fires it again. ```json { - "id": "evt_...", + "id": "6f1c9a02-4d3b-4c1e-9f77-2b8a5d0e1c34", "type": "subscription.created", "created_at": 1765467600000, "data": { - "athlete_id": "a1b2c3d4-...", + "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "tier": "full", "expires_at": 1797003600000, "source": "checkout_subscription" @@ -92,17 +103,17 @@ console.log(`Webhook secret: ${webhook.secret}`); } ``` -`source` values: `checkout_subscription`, `checkout_lifetime` (no `expires_at`), `existing_subscription_linked` (an already-subscribed Saturday account tapped your CTA — they were linked without being charged), `email_match` (a paying Saturday account was linked to your athlete automatically by email — see [Freemium Model → Automatic linking by email](/guides/freemium-model#automatic-linking-by-email)), `simulated` (test env only). +`source` values: `checkout_subscription`, `checkout_lifetime` (no `expires_at`), `existing_subscription_linked` (an already-subscribed Saturday account tapped your CTA and was linked without being charged), `email_match` (a paying Saturday account was linked to your athlete automatically by email, see [Freemium Model, Automatic linking by email](/guides/freemium-model#automatic-linking-by-email)), `simulated` (test env only). `subscription.cancelled` means the athlete's next calculate returns teaser ranges: ```json { - "id": "evt_...", + "id": "8a2d7e14-9b60-4f33-a1c5-7d4e6b90f218", "type": "subscription.cancelled", "created_at": 1765467600000, "data": { - "athlete_id": "a1b2c3d4-...", + "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "tier": "teaser", "reason": "customer.subscription.deleted" } @@ -110,33 +121,44 @@ console.log(`Webhook secret: ${webhook.secret}`); ``` - Don't cache tier from webhooks alone — `GET /v1/athletes/{id}` returns a computed `subscription_status` (`full` | `trial` | `teaser`) whenever you need ground truth, e.g. when an athlete returns from checkout. + Don't cache tier from webhooks alone. `GET /v1/athletes/{id}` returns a computed `subscription_status` (`full` | `trial` | `teaser`) whenever you need ground truth, for example when an athlete returns from checkout. ## Webhook payload format -Every webhook delivery has this structure: +Every webhook delivery has this structure. `id` is a UUID and `created_at` is a Unix timestamp in **milliseconds**: ```json { - "id": "evt_abc123def456", + "id": "3e9b1f27-5c84-4a19-b2d6-8f70c3a51e4b", "type": "prescription.calculated", - "created_at": "2025-01-15T14:30:00Z", + "created_at": 1765467600000, "data": { - "athlete_id": "ath_abc123", - "activity_id": "act_xyz789", + "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", + "activity_id": "7c5e2b90-1a3f-4d68-9e02-5b7a4c1d8f36", "carb_g_per_hr": 60, - "sodium_mg_per_hr": 500, - "fluid_ml_per_hr": 600 + "sodium_mg_per_hr": 500, + "fluid_ml_per_hr": 600 } } ``` +The `data` object is the same resource body the REST API returns for that object, so `athlete.*` events carry the athlete record and `prescription.calculated` carries the calculate response. Athlete, activity, and event identifiers are all UUIDs; they carry no prefix, so do not pattern-match on one. + ## Verifying webhook signatures (HMAC-SHA256) -Every webhook delivery includes a signature in the `X-Saturday-Signature` header. **Always verify this signature** to confirm the webhook came from Saturday and wasn't tampered with. +Every delivery carries these headers: + +| Header | Value | +|--------|-------| +| `X-Saturday-Signature` | `t={unix_seconds},v1={hex_hmac}` | +| `X-Saturday-Event-ID` | The event `id`, matching the `id` in the body | +| `X-Saturday-Event-Type` | The event `type`, so you can route before parsing | +| `User-Agent` | `Saturday-Webhooks/1.0` | -The signature is computed as `HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)`. +Verify the signature on every request. It is the only thing distinguishing a Saturday delivery from anyone who has guessed your endpoint URL. + +The signature is computed as `HMAC-SHA256(webhook_secret, timestamp + "." + raw_body)`, where `timestamp` is the `t` value from the header in Unix seconds and `raw_body` is the exact bytes received. Parsing and re-serializing the JSON before verifying will change the bytes and break the comparison. ### Verification steps @@ -179,7 +201,7 @@ def handle_webhook(): # 3. Reject stale timestamps (5-minute replay window) if abs(time.time() - int(timestamp)) > 300: - abort(400, "Timestamp too old — possible replay attack") + abort(400, "Timestamp outside the replay window") # 4. Compute expected signature raw_body = request.get_data(as_text=True) @@ -198,7 +220,7 @@ def handle_webhook(): event = request.json print(f"Received event: {event['type']}") - # Always return 200 quickly — process async if needed + # Return 200 quickly; process async if needed return "", 200 ``` @@ -262,49 +284,51 @@ app.post( ## Retry behavior -If your endpoint fails (non-2xx response or timeout), Saturday retries with exponential backoff: +A delivery counts as failed if your endpoint returns a non-2xx status, times out, or cannot be reached. Saturday then retries on this schedule: + +| Attempt | Delay after previous attempt | Elapsed since first attempt | +|---------|------------------------------|------------------------------| +| 1st retry | 1 minute | 1 min | +| 2nd retry | 5 minutes | 6 min | +| 3rd retry | 30 minutes | 36 min | +| 4th retry | 2 hours | 2 hr 36 min | +| 5th retry | 12 hours | 14 hr 36 min | -| Attempt | Delay | Total elapsed | -|---------|-------|---------------| -| 1st retry | 30 seconds | 30s | -| 2nd retry | 2 minutes | 2.5 min | -| 3rd retry | 10 minutes | 12.5 min | -| 4th retry | 1 hour | 1 hr 12.5 min | -| 5th retry | 4 hours | 5 hr 12.5 min | +Retries are scheduled durably, so they survive a restart on Saturday's side. Each retry re-reads your webhook's current URL and secret, which means fixing a bad URL mid-schedule lets the remaining retries land. After the 5th retry the delivery is marked exhausted and is not attempted again. -After 5 failed retries, the delivery is marked as failed and the event is logged for manual retry. +Redirects are not followed: a `3xx` counts as a failure, so register the final URL rather than a redirector. ## Auto-disable -If a webhook endpoint fails consistently for **3 consecutive days**, Saturday automatically disables it and sends a notification email to the partner contact. Re-enable it from the API after fixing the issue: +Each exhausted delivery increments a consecutive-failure counter on the webhook. Any successful delivery resets it to zero. At **15 consecutive exhausted deliveries** Saturday sets the webhook inactive and stops sending to it. Given the retry schedule above, that is roughly three days of an endpoint being consistently down, though the exact wall time depends on your event volume. + +Re-enable it from the API once the endpoint is healthy: ```bash curl -X PATCH https://api.saturday.fit/v1/webhooks/{webhook_id} \ - -H "Authorization: Bearer sk_live_xyz789..." \ + -H "Authorization: Bearer $SATURDAY_API_KEY" \ -d '{"active": true}' ``` ## Best practices -1. **Return 200 immediately** — process events asynchronously. Saturday times out after 30 seconds. -2. **Handle duplicates** — use the `id` field to deduplicate. The same event may be delivered more than once. -3. **Verify signatures** — always. Never trust a webhook payload without HMAC verification. -4. **Use HTTPS** — Saturday only delivers to HTTPS endpoints. -5. **Log everything** — store raw payloads for debugging. Include the event `id` in your logs. +1. **Return 200 immediately**, then process asynchronously. Saturday closes the connection 10 seconds after the request starts, and a slow handler burns retries. +2. **Handle duplicates** using the `id` field. Delivery is at-least-once, so the same event can arrive more than once. +3. **Verify the signature** on every request before acting on the payload. +4. **Serve HTTPS on port 443.** Production registration requires `https`; the test environment also accepts `http`, and both environments reject non-standard ports. +5. **Log the raw body and the event `id`** before parsing. Signature failures are almost always a body-mutation problem, and the raw bytes are what let you prove it. ## Managing webhooks -**List webhooks:** ```bash -GET /v1/webhooks +GET /v1/webhooks # list your webhooks +GET /v1/webhooks/{webhook_id} # read one +PATCH /v1/webhooks/{webhook_id} # change url, events, or active +DELETE /v1/webhooks/{webhook_id} # remove it +POST /v1/webhooks/{webhook_id}/test # send a webhook.test event now +GET /v1/webhooks/{webhook_id}/deliveries # recent delivery attempts ``` -**Update events or URL:** -```bash -PATCH /v1/webhooks/{webhook_id} -``` +`POST /v1/webhooks/{webhook_id}/test` delivers synchronously and returns the delivery record, including the status code your endpoint returned. Use it to confirm signature verification works before you depend on live events. The test event has type `webhook.test`, which is not a subscribable event type; your handler should ignore types it does not recognize rather than erroring on them. -**Delete a webhook:** -```bash -DELETE /v1/webhooks/{webhook_id} -``` +`GET /v1/webhooks/{webhook_id}/deliveries` returns the 50 most recent attempts for that webhook, newest first, each with its status, attempt count, and the first kilobyte of your endpoint's response body, which is usually enough to see why a delivery failed. diff --git a/introduction.mdx b/introduction.mdx index d0a0136..537535b 100644 --- a/introduction.mdx +++ b/introduction.mdx @@ -1,74 +1,72 @@ --- title: 'Introduction' -description: 'Nutrition intelligence for endurance athletes — as an API' +description: 'Fuel, hydration, and electrolyte prescriptions for endurance athletes, as an API' sidebarTitle: 'Introduction' --- # Saturday API -Saturday provides AI-powered nutrition intelligence for endurance athletes. Our API calculates personalized fuel, hydration, and electrolyte recommendations based on athlete profiles, activity parameters, and environmental conditions. +Saturday calculates personalized fuel, hydration, and electrolyte prescriptions for endurance athletes. Send an activity and an athlete profile, get back carbohydrate, sodium, and fluid targets along with the safety metadata that bounds them. -## What Saturday does +## What the API returns -Training platforms build great workout plans. Saturday makes sure athletes are properly fueled for them. +Training platforms build the workout. Saturday fuels it. -The API takes training data in and returns science-backed, safety-guarded nutrition prescriptions out: +| Output | Field | Units | +|--------|-------|-------| +| Carbohydrate target | `carb_g_per_hr` | grams per hour | +| Hydration target | `fluid_ml_per_hr` | milliliters per hour | +| Sodium target | `sodium_mg_per_hr` | milligrams per hour | +| Session totals | `total_carb_g`, `total_sodium_mg`, `total_fluid_ml` | for the whole activity | +| Safety metadata | `safety` | ceilings, warnings, confidence, human-review flag | -- **Carbohydrate targets** — grams per hour, personalized to the athlete and activity -- **Hydration targets** — fluid intake in mL per hour, adjusted for sweat rate and conditions -- **Electrolyte targets** — sodium in mg per hour, accounting for individual sweat composition -- **Product recommendations** — matched from a curated database of 186+ endurance nutrition products -- **AI coaching** — conversational nutrition coach with SSE streaming for real-time interactions -- **Safety metadata** — every response includes safety guardrails and risk flags +Safety metadata is present on every response at every tier, including free ones. See [Safety](/guides/safety). -## Why an API? +Product matching against Saturday's curated endurance-nutrition catalog and the conversational AI coach are built but gated, and are not open to new integrations today. [Feature Gates](/guides/feature-gates) covers the launch stages and how to request access. -Nutrition for endurance athletes is safety-critical. Hyponatremia (dangerously low blood sodium from overdrinking) kills athletes. Bad fueling causes GI distress, bonking, and DNFs. Building a nutrition engine that handles these edge cases correctly requires deep sports science expertise and years of algorithm development. +## The problem -Saturday's API lets training platforms offer nutrition intelligence without building it from scratch. Your platform handles the training. Saturday handles the fueling. +Fueling for endurance athletes is safety-critical. Hyponatremia, dangerously low blood sodium from overdrinking, kills athletes. Bad fueling causes GI distress, bonking, and DNFs. Getting those edge cases right takes sports-science depth and years of algorithm work. -## Who it's for +Saturday's API lets a platform offer that without building it. Your platform handles the training. Saturday handles the fueling. -The Saturday API is designed for developers at: +## Who builds on it -- **Training platforms** — Athletica.ai, Intervals.icu, TriDot, AI Endurance, Final Surge -- **Wearable companies** — Integrate nutrition into watch faces and workout summaries -- **Race organizers** — Provide personalized fueling plans for event participants -- **Coaching platforms** — Give coaches nutrition tools alongside training tools -- **AI agents** — Consume Saturday via MCP or direct API for automated nutrition workflows +- Training platforms, the Athletica.ai / Intervals.icu / TriDot / AI Endurance / Final Surge category +- Wearable companies putting nutrition into watch faces and workout summaries +- Race organizers issuing fueling plans to event participants +- Coaching platforms adding nutrition tools alongside training tools +- AI agents consuming Saturday over MCP or direct HTTP -## Free for partners +## What it costs -The API itself is free for partners to integrate. Athletes access teaser data (ranges instead of exact numbers) at no cost. For full precision, athletes subscribe to Saturday directly. Partners earn attribution and provide a better experience — without managing nutrition billing. +Platform partners integrate at no charge and earn attribution. Athletes who want exact numbers subscribe to Saturday directly, so the partner carries no nutrition billing. Builders whose athletes live in their own system use the self-serve individual plan at $5.39/month. [Getting Access](/access) routes you to the right lane. -See [Freemium Model](/guides/freemium-model) for details on how teaser vs. full responses work. +Athletes without a subscription get teaser responses: ranges in place of exact numbers, safety metadata intact. [Freemium Model](/guides/freemium-model) shows both response shapes. -## Core capabilities +## Core guides -| Capability | Description | +| Guide | Covers | |------------|-------------| -| [Nutrition Calculation](/guides/nutrition-calculation) | The core product — calculate fuel prescriptions | -| [Athletes](/guides/athletes) | Manage athlete profiles scoped to your organization | -| [Activities](/guides/activities) | Create activities and get prescriptions | -| [AI Coach](/guides/ai-coach) | Conversational nutrition coaching with SSE streaming | -| [Webhooks](/guides/webhooks) | Real-time event notifications for your integration | -| [Safety](/guides/safety) | Understand Saturday's safety model — this is life-or-death | +| [Nutrition Calculation](/guides/nutrition-calculation) | The prescription endpoint and its inputs | +| [Athletes](/guides/athletes) | Athlete profiles scoped to your organization | +| [Activities](/guides/activities) | Creating activities and attaching prescriptions | +| [Webhooks](/guides/webhooks) | Event notifications for your integration | +| [Safety](/guides/safety) | Saturday's safety model, and why it is load-bearing | ## Getting started -The fastest path to your first API response: - - Make your first API call in 5 minutes + Your first prescription, start to finish API keys, environments, and security - Understand why safety metadata matters + What the safety metadata means - The core endpoint — calculate a fuel prescription + The prescription endpoint in depth diff --git a/llms.txt b/llms.txt index e864d97..b1e802e 100644 --- a/llms.txt +++ b/llms.txt @@ -1,40 +1,57 @@ # Saturday API -> Nutrition intelligence for endurance athletes — as an API. +> Nutrition intelligence for endurance athletes, as an API. -Saturday provides AI-powered nutrition intelligence for endurance athletes. The API calculates personalized fuel, hydration, and electrolyte recommendations based on athlete profiles, activity parameters, and environmental conditions. +Saturday calculates personalized fuel, hydration, and electrolyte recommendations for endurance athletes from athlete profiles, activity parameters, and environmental conditions. ## Authentication -All requests require a Bearer token in the Authorization header. API keys use the `sk_test_` (sandbox) or `sk_live_` (production) prefix. +All requests require a Bearer token in the Authorization header. Partner API keys use the `sk_test_` (sandbox) or `sk_live_` (production) prefix. ``` -Authorization: Bearer sk_test_abc123def456 +Authorization: Bearer $SATURDAY_API_KEY ``` +Coaches have their own surface with its own credentials: a `cp_live_` / `cp_test_` coach API key, or an OAuth2 token carrying coach scopes. + ## Core Endpoints -- POST /v1/nutrition/calculate — Calculate fuel/hydration/electrolyte prescription -- POST /v1/nutrition/calculate/batch — Batch calculate for multiple scenarios -- POST /v1/nutrition/products/compare — Compare products against a prescription -- POST /v1/athletes — Create an athlete profile -- GET /v1/athletes — List partner's athletes -- POST /v1/athletes/{id}/activities — Create an activity -- POST /v1/athletes/{id}/activities/{activityId}/calculate — Calculate prescription -- POST /v1/ai/conversations — Start AI coaching conversation (SSE streaming) -- POST /v1/webhooks — Register webhook for real-time events +- POST /v1/nutrition/calculate: calculate a fuel/hydration/electrolyte prescription +- POST /v1/nutrition/calculate/batch: batch calculate for multiple scenarios +- POST /v1/nutrition/products/compare: compare products against a prescription +- POST /v1/athletes: create an athlete profile +- GET /v1/athletes: list the partner's athletes +- POST /v1/athletes/{athlete_id}/activities: create an activity +- POST /v1/athletes/{athlete_id}/activities/{activity_id}/calculate: calculate that activity's prescription +- POST /v1/ai/conversations: start an AI coaching conversation (SSE streaming) +- POST /v1/webhooks: register a webhook for real-time events + +## Coach Endpoints + +Requires the Pro Coach tier or above. Every athlete id is confined to the coach's roster. + +- GET /v1/coach/roster: the roster with per-athlete needs-attention markers +- GET /v1/coach/roster/digest: flagged athletes only, most-flagged first +- GET /v1/coach/athletes/{athlete_uid}/fueling-rollup: in-window sessions plus the concern summary +- GET /v1/coach/athletes/{athlete_uid}/report: the AI fueling report, narrative plus structured data +- PUT /v1/coach/config/notification-rules: replace the alert rules at a scope +- POST /v1/coach/webhooks: register a coach webhook endpoint ## Safety -Every response includes safety metadata (confidence_score, warnings, max_safe_fluid_ml_per_hr, max_safe_sodium_mg_per_hr, not_instructions). Safety data is NEVER gated behind subscription status. Bad hydration recommendations can cause exercise-associated hyponatremia — a condition that kills athletes. +Every response includes safety metadata (confidence_score, warnings, max_safe_fluid_ml_per_hr, max_safe_sodium_mg_per_hr, not_instructions). Safety data is never gated behind subscription status. Bad hydration recommendations can cause exercise-associated hyponatremia, which kills athletes. ## Freemium Model -The API is free for partners. Subscribed athletes get exact numbers; free athletes get ranges (teasers). Safety data is always complete regardless of subscription status. +Subscribed athletes get exact numbers; free athletes get ranges (teasers). Safety data is complete either way. + +## Data Use + +Every response carries an `X-Saturday-Data-License` header. Response data is licensed for display to end users only. Training or fine-tuning models on it, aggregating it, reverse engineering the calculations, and redistributing it are all prohibited. See https://docs.saturday.fit/guides/data-policy. ## Documentation -- Full docs: https://api.saturday.fit -- OpenAPI spec: https://api.saturday.fit/openapi.yaml +- Full docs: https://docs.saturday.fit +- Full LLM context: https://docs.saturday.fit/llms-full.txt - MCP server: https://api.saturday.fit/mcp -- Full LLM context: https://api.saturday.fit/llms-full.txt +- API root discovery: https://api.saturday.fit/v1/ diff --git a/quickstart.mdx b/quickstart.mdx index aa2f09e..4becbd5 100644 --- a/quickstart.mdx +++ b/quickstart.mdx @@ -1,36 +1,38 @@ --- title: 'Quickstart' -description: '5 minutes to your first API call' +description: 'Your first prescription, start to finish' sidebarTitle: 'Quickstart' --- # Quickstart -This guide gets you from zero to a working nutrition calculation in under 5 minutes. - ## 1. Get your API key -**Self-serve (most builders):** sign up at [saturday.fit/api](https://saturday.fit/api) — $5.39/month, no contract. Your key is revealed once after checkout and emailed to you. An AI agent can do this for you too: `POST /v1/signup` (no auth) returns a hosted checkout link. Not sure self-serve is your lane? See [Getting Access](/access). +**Self-serve:** sign up at [saturday.fit/api](https://saturday.fit/api) for $5.39/month, no contract. Your key is revealed once after checkout and emailed to you. An AI agent can do this for you: `POST /v1/signup` (no auth) returns a hosted checkout link. Not sure self-serve is your lane? See [Getting Access](/access). -**Platform partners:** keys come with your agreement — contact [api@saturday.fit](mailto:api@saturday.fit). Partner sandbox keys start with `sk_test_` and work against test data with no rate limit concerns. +**Platform partners:** keys come with your agreement. Contact [api@saturday.fit](mailto:api@saturday.fit). -``` -sk_live_abc123def456... -``` +Keys carry an environment prefix. Production keys start with `sk_live_`, sandbox keys with `sk_test_`, and the two are not interchangeable: a sandbox key does not authenticate against production. See [Authentication](/authentication#environments) for what each environment does and which base URL it uses. - Treat your key like a password — it authenticates every request and spends your daily call allowance. + Treat your key like a password. It authenticates every request and spends your daily call allowance. +The examples below read the key from a `SATURDAY_API_KEY` environment variable, so the same code runs against either environment: + +```bash +export SATURDAY_API_KEY="sk_live_..." +``` + ## 2. Make your first calculation -The core of Saturday's API is the nutrition calculation endpoint. It takes activity parameters and returns a fuel prescription. +The prescription endpoint takes activity parameters and returns fuel, hydration, and sodium targets. Only `activity_type` and `duration_min` are required; everything else sharpens the result. ```bash cURL curl -X POST https://api.saturday.fit/v1/nutrition/calculate \ - -H "Authorization: Bearer sk_test_abc123def456" \ + -H "Authorization: Bearer $SATURDAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "activity_type": "run", @@ -42,12 +44,13 @@ curl -X POST https://api.saturday.fit/v1/nutrition/calculate \ ``` ```python Python +import os import requests response = requests.post( "https://api.saturday.fit/v1/nutrition/calculate", headers={ - "Authorization": "Bearer sk_test_abc123def456", + "Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}", "Content-Type": "application/json", }, json={ @@ -69,7 +72,7 @@ const response = await fetch( { method: "POST", headers: { - Authorization: "Bearer sk_test_abc123def456", + Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ @@ -90,7 +93,7 @@ console.log(data); ### Install an SDK (optional) -Saturday provides official SDKs for Python and TypeScript: +Saturday publishes SDKs for Python and TypeScript: @@ -107,9 +110,10 @@ npm install @saturdayinc/sdk ```python Python (SDK) +import os from saturday import Saturday -client = Saturday(api_key="sk_test_abc123def456") +client = Saturday(api_key=os.environ["SATURDAY_API_KEY"]) result = client.nutrition.calculate( activity_type="run", @@ -125,7 +129,7 @@ print(result) ```typescript TypeScript (SDK) import Saturday from "@saturdayinc/sdk"; -const client = new Saturday({ apiKey: "sk_test_abc123def456" }); +const client = new Saturday({ apiKey: process.env.SATURDAY_API_KEY }); const result = await client.nutrition.calculate({ activity_type: "run", @@ -140,19 +144,21 @@ console.log(result); +Both SDKs default to `https://api.saturday.fit` and accept a base URL override (`base_url` in Python, `baseUrl` in TypeScript) for sandbox work. + ## 3. Read the response -A successful response returns a fuel prescription with safety metadata: +A successful response returns a prescription with safety metadata: ```json { "tier": "full", - "carb_g_per_hr": 62.5, - "sodium_mg_per_hr": 485.0, + "carb_g_per_hr": 60.0, + "sodium_mg_per_hr": 600.0, "fluid_ml_per_hr": 620.0, - "total_carb_g": 125, - "total_sodium_mg": 970, - "total_fluid_ml": 1240, + "total_carb_g": 90, + "total_sodium_mg": 900, + "total_fluid_ml": 930, "safety": { "max_safe_fluid_ml_per_hr": 1500, "max_safe_sodium_mg_per_hr": 3000, @@ -170,65 +176,76 @@ A successful response returns a fuel prescription with safety metadata: } ``` -Key fields to understand: - | Field | What it means | |-------|---------------| -| `tier` | Response tier — `"full"` for subscribed athletes, `"teaser"` for free | +| `tier` | `"full"` for exact numbers, `"teaser"` for ranges | | `carb_g_per_hr` | Carbohydrate target in grams per hour | | `sodium_mg_per_hr` | Sodium target in milligrams per hour | | `fluid_ml_per_hr` | Fluid target in milliliters per hour | -| `total_*` | Total amounts for the entire activity duration | -| `safety.confidence_score` | How confident Saturday is in this prescription (0.0-1.0) | -| `safety.warnings` | Any safety warnings for this prescription | -| `safety.not_instructions` | Signals that prescriptions are guidance, not commands | +| `total_*` | Totals across the whole activity duration | +| `safety.max_safe_*_per_hr` | Hard ceilings the prescription is held under | +| `safety.confidence_score` | Confidence in this prescription, 0.0 to 1.0 | +| `safety.requires_human_review` | Set when the case warrants a dietitian's eyes | +| `safety.warnings` | Safety warnings for this prescription | +| `safety.not_instructions` | Marks the prescription as guidance, not commands | + +A response may also carry a `precision` object describing which profile fields are still missing and how much they widen the answer. See [Onboarding](/guides/onboarding) for how to collect them. - **Safety data is never gated.** Every response includes full safety metadata regardless of subscription status. Bad hydration can cause hyponatremia — a potentially fatal condition. Safety information is always free and complete. + Safety data is never gated. Every response carries full safety metadata regardless of subscription status, because overdrinking can cause hyponatremia, a potentially fatal condition. -## 4. Understand teaser vs. full responses +## 4. Teaser and full responses -The response above shows **full precision** numbers because it's from a sandbox key. In production, responses depend on the athlete's subscription status: +`tier` reflects the athlete's subscription and trial status, not your key's environment. A request with no athlete attached returns full precision. | Athlete status | What they get | Example | |----------------|---------------|---------| -| **Subscribed** | Exact numbers | `"carb_g_per_hr": 62.5` | -| **Free / teaser** | Ranges | `"carb_range_g_per_hr": "60-80"` | +| **Subscribed or in trial** | Exact numbers | `"carb_g_per_hr": 60.0` | +| **Free** | Ranges | `"carb_range_g_per_hr": "60-90"` | -Teaser responses include a `subscription_cta` field: +Teaser responses carry a `subscription_cta` field, and their `attribution.required` is `true`: ```json { "tier": "teaser", - "carb_range_g_per_hr": "60-80", - "sodium_range_mg_per_hr": "300-600", - "fluid_range_ml_per_hr": "600-900", + "carb_range_g_per_hr": "60-90", + "sodium_range_mg_per_hr": "500-1000", + "fluid_range_ml_per_hr": "500-1000", + "attribution": { + "text": "Powered by Saturday", + "logo_url": "https://saturday.fit/logo.png", + "link": "https://saturday.fit", + "required": true + }, "subscription_cta": { - "message": "Subscribe to Saturday for exact fueling targets", + "message": "Get your exact carb, sodium, and fluid targets, not ranges", "subscribe_url": "https://saturday.fit/subscribe?ref=YOUR_PARTNER_ID", - "features": ["Exact carb/sodium/fluid targets", "Product recommendations"] + "features": [ + "Exact gram/mg/mL targets per hour", + "Personalized product picks inside the app", + "Personalized fueling plan", + "15+ tuning factors" + ] } } ``` -See [Freemium Model](/guides/freemium-model) for the full details. +See [Freemium Model](/guides/freemium-model) for how the tiers are decided and how the subscribe loop pays you. ## 5. Next steps -You've just calculated a nutrition prescription with minimal inputs. From here: - - [Athletes](/guides/athletes) — Store athlete settings (weight, sweat level, preferences) for more accurate calculations. + [Athletes](/guides/athletes) stores athlete settings (weight, sweat level, preferences) so calculations stop relying on defaults. - [Safety](/guides/safety) — Learn about Saturday's safety model and why it matters. + [Safety](/guides/safety) covers the safety model and the ceilings above. - [Activities](/guides/activities) — Create activities, attach prescriptions, and collect feedback. + [Activities](/guides/activities) creates activities, attaches prescriptions, and collects post-session feedback. - [Authentication](/authentication) — Swap your `sk_test_` key for a `sk_live_` key when you're ready. + [Authentication](/authentication) covers swapping a `sk_test_` key and its sandbox base URL for a `sk_live_` key against `api.saturday.fit`. diff --git a/rate-limiting.mdx b/rate-limiting.mdx index a208375..a88d7c5 100644 --- a/rate-limiting.mdx +++ b/rate-limiting.mdx @@ -1,48 +1,67 @@ --- title: 'Rate Limiting' -description: 'Rate limit headers, tiers, and handling exceeded responses' +description: 'Rate limit headers, ceilings, and handling 429s' sidebarTitle: 'Rate Limiting' --- # Rate Limiting -Saturday uses token bucket rate limiting to ensure fair access and API stability. Rate limits are applied per API key. +Saturday meters requests with a token bucket per account, not per key. Several keys on one account draw from the same bucket. + +Two ceilings apply: + +| Ceiling | What it bounds | Reset | +|---------|----------------|-------| +| Token bucket | Sustained rate, with a burst allowance on top | Continuous refill | +| Daily call limit | Total calls per day, on accounts that carry one | Midnight UTC | + +Self-serve accounts run at 2 requests/second with a burst of 5, and 200 calls/day. Platform partner limits are set per agreement and carry no daily ceiling by default. A self-serve account can read its own current numbers from `GET https://api.saturday.fit/v1/` without authenticating. ## Rate limit headers -Every API response includes headers showing your current rate limit status: +Every authenticated response carries the current bucket state: ```http HTTP/1.1 200 OK -X-RateLimit-Limit: 60 -X-RateLimit-Remaining: 42 -X-RateLimit-Reset: 1705312800 +X-RateLimit-Limit: 120 +X-RateLimit-Remaining: 4 +X-RateLimit-Reset: 0 Content-Type: application/json ``` | Header | Description | |--------|-------------| -| `X-RateLimit-Limit` | Maximum requests allowed per minute | -| `X-RateLimit-Remaining` | Requests remaining in the current window | -| `X-RateLimit-Reset` | Unix timestamp when the window resets | +| `X-RateLimit-Limit` | Sustained rate expressed as requests per minute | +| `X-RateLimit-Remaining` | Tokens left in the burst bucket right now | +| `X-RateLimit-Reset` | Seconds until the next token is available. `0` while tokens remain | + +`X-RateLimit-Reset` is a delta in seconds, not a Unix timestamp. Adding it to the current time gives the moment the next request will be admitted. ## When you're rate limited -When you exceed the limit, Saturday returns a `429 Too Many Requests` response: +Exceeding a ceiling returns `429 Too Many Requests`: ```json { "error": { - "type": "rate_limit_exceeded", + "type": "rate_limit_error", "code": "rate_limit_exceeded", - "message": "Rate limit exceeded. Retry after 30 seconds.", - "documentation_url": "https://api.saturday.fit/docs/rate-limiting", + "message": "Rate limit exceeded. Retry after 1 seconds.", + "documentation_url": "https://docs.saturday.fit/errors#rate_limit_exceeded", "request_id": "req_abc123def456" } } ``` -Always respect the `Retry-After` header: +Three cases produce a `rate_limit_error`, and they want different handling: + +| Code | Cause | `Retry-After` | What to do | +|------|-------|---------------|------------| +| `rate_limit_exceeded` | Token bucket empty | Present | Wait it out, then continue | +| `rate_limit_exceeded` | Daily call ceiling reached | Absent | Stop until midnight UTC. The message states the limit | +| `rate_limited` | Request pattern flagged as a calculator-extraction sweep | Absent | Vary your inputs, or contact support if the traffic is legitimate | + +Read `Retry-After` with a fallback rather than indexing it, since two of the three cases omit it: @@ -52,7 +71,7 @@ import time response = make_api_request() if response.status_code == 429: - retry_after = int(response.headers["Retry-After"]) + retry_after = int(response.headers.get("Retry-After", 60)) time.sleep(retry_after) response = make_api_request() ``` @@ -69,39 +88,39 @@ if (response.status === 429) { -## Best practices +Athlete-delegated callers (OAuth2 and the Claude connector) also pass through a per-athlete bucket inside the account's bucket, so one heavy user cannot starve the rest. Those 429s carry a longer `Retry-After`. See [OAuth2](/guides/oauth2). -### Spread requests evenly +## Staying under the ceilings -Instead of bursting 60 requests in 5 seconds then waiting 55 seconds, spread them across the full minute. Token bucket rate limiting allows short bursts but sustained bursting will hit the limit. +### Spread requests out -### Cache when possible +The burst allowance covers a short spike, then refills at the sustained rate. Pacing requests across the window keeps the bucket from emptying; draining the burst up front means every subsequent request waits. -Nutrition prescriptions for the same inputs don't change unless the athlete's profile is updated. Cache prescription results and only recalculate when: +### Cache prescriptions -- The athlete's profile settings change -- Activity parameters change (new weather forecast, updated duration) -- You need a fresh calculation for race day +A prescription for the same inputs doesn't change on its own. Cache the result and recalculate when: -### Use batch endpoints +- The athlete's profile settings change +- Activity parameters change, such as an updated duration or a new weather forecast +- You want a fresh calculation for race day -Instead of making 10 individual calculation requests, use the batch endpoint: +### Use the batch endpoint ```bash POST /v1/nutrition/calculate/batch ``` -Batch requests count as one API call per item for rate limiting but avoid connection overhead. +Each item in a batch debits a token, so a batch of 10 costs what 10 calls cost. What it saves is round trips, not quota. See [Batch Operations](/guides/batch-operations). -### Monitor your usage - -Check your current usage via the API: +### Watch your usage ```bash curl https://api.saturday.fit/v1/partner/usage \ - -H "Authorization: Bearer sk_live_xyz789..." + -H "Authorization: Bearer $SATURDAY_API_KEY" ``` +`GET /v1/partner/usage/daily` breaks the same counters down by day. + ## Requesting higher limits -If your integration needs higher limits, contact [api@saturday.fit](mailto:api@saturday.fit) with your partner ID and expected volume. Limit upgrades are free — we want your integration to succeed. +Outgrowing the ceilings is the point at which we want to hear from you. Send your account or partner ID and expected volume to [support@saturday.fit](mailto:support@saturday.fit). Raising a limit costs nothing. diff --git a/snippets/app-action.jsx b/snippets/app-action.jsx index a790954..5476c55 100644 --- a/snippets/app-action.jsx +++ b/snippets/app-action.jsx @@ -1,25 +1,25 @@ -// app-action.jsx — Saturday docs reusable snippet. -// Makes a doc instruction clickable so a coach lands in the actual Saturday app. -// Mobile: a normal-looking bold link that the OS hands to the app (universal link). -// Desktop: an identically-styled inline button that pops a "scan to open on phone" QR modal. +// app-action.jsx: Saturday docs reusable snippet. +// Makes a doc instruction clickable so a coach lands in the Saturday app. +// Mobile: a bold link that the OS hands to the app (universal link). +// Desktop: an identically-styled inline button that opens a "scan to open on phone" QR modal. // // Mintlify sandbox constraints honored here: -// - React hooks (useState/useEffect) are PRE-INJECTED — no React import. -// - No external npm packages — the QR is a remote image endpoint, not a qr library. +// - React hooks (useState/useEffect) are pre-injected, so there is no React import. +// - No external npm packages, so the QR is a remote image endpoint rather than a qr library. // - Named export only, no default export, no cross-snippet imports. -// - navigator.* is browser-only — touched solely inside useEffect (undefined during SSR). +// - navigator.* is browser-only, so it is touched solely inside useEffect (undefined during SSR). // AppAction: clickable instruction that opens the Saturday app for the destination `path`. -// NOTE: Mintlify evaluates the EXPORTED component in isolation — a module-scope sibling -// (a top-level `const LINK_STYLE`) is NOT in scope at render time and throws -// "ReferenceError: LINK_STYLE is not defined", rendering the component empty. So every -// value the component needs lives INSIDE the function body; only the pre-injected hooks -// (useState/useEffect) come from outside. +// Mintlify evaluates the exported component in isolation, so a module-scope sibling (a +// top-level `const LINK_STYLE`) is not in scope at render time and throws +// "ReferenceError: LINK_STYLE is not defined", rendering the component empty. Every value +// the component needs therefore lives inside the function body; only the pre-injected +// hooks (useState/useEffect) come from outside. export const AppAction = ({ path = "/open-app", children }) => { - // Shared inline style so the mobile link and the desktop button read IDENTICALLY + // Shared inline style so the mobile link and the desktop button read identically // inside a sentence: bold, brand teal, subtle underline that strengthens on hover. const LINK_STYLE = { - fontWeight: 600, // semibold — reads as a strong doc link + fontWeight: 600, // semibold: reads as a strong doc link color: "#1aabb8", // Saturday brand teal cursor: "pointer", textDecoration: "underline", @@ -34,12 +34,12 @@ export const AppAction = ({ path = "/open-app", children }) => { // Universal/deep link the OS resolves to the installed app (or the web fallback). const deepUrl = "https://saturday.fit" + path; - // isMobile starts false so the FIRST (server) render is always the desktop branch — - // this keeps SSR from ever touching navigator and avoids a hydration mismatch crash. + // isMobile starts false so the first (server) render is always the desktop branch, + // keeping SSR from touching navigator and avoiding a hydration mismatch crash. const [isMobile, setIsMobile] = useState(false); const [showQR, setShowQR] = useState(false); - // Device sniff runs ONLY in the browser, after mount — navigator is safe here. + // Device sniff runs only in the browser, after mount, where navigator is safe. useEffect(() => { setIsMobile(/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)); }, []); @@ -52,7 +52,7 @@ export const AppAction = ({ path = "/open-app", children }) => { e.currentTarget.style.textDecorationColor = "rgba(26, 171, 184, 0.4)"; }; - // MOBILE — render a real anchor; tapping lets the OS open the Saturday app. + // Mobile: render an anchor, so tapping lets the OS open the Saturday app. if (isMobile) { return ( { "https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=" + encodeURIComponent(deepUrl); - // DESKTOP — an inline button styled exactly like the mobile link; opens the QR modal. + // Desktop: an inline button styled like the mobile link, opening the QR modal. return ( <>