# Getting Access Source: https://docs.saturday.fit/access Which lane fits you: self-serve API, coach portal, or partnership # Getting Access There are four ways to build with Saturday. One question routes you: where do your athletes live? | Lane | Who | Athletes live in… | Auth | Money | | -------------------- | ------------------------------------------------------------------ | ------------------------ | ----------------------------- | --------------------------------------------------------- | | Consumer app | An athlete | the Saturday app | app account | app subscription | | **Coach portal** | A coach managing real Saturday-app athletes | **the Saturday app** | portal login + `cp_` API keys | Pro Coach+ subscription (portal); `cp_` keys on Business+ | | **Individual API** | Dev-athletes, AI-assisted coaching businesses, team-scale builders | **your own system** | self-serve `sk_` key | \$5.39/month, no contract | | **Platform partner** | A platform embedding Saturday for its whole user base | **the partner's system** | `sk_` key via agreement | rev share + athlete subscribe loop | ## Individual API (self-serve) 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). 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 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. 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#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. ## Discovery for agents `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). # Authentication Source: https://docs.saturday.fit/authentication API keys, OAuth2, environments, and securing your integration # Authentication 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 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, covered in the [Coach API](/guides/coach-api). ## API keys All server-to-server requests pass the key in the `Authorization` header as a Bearer token. ### Key types 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 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 **[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. 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. ```bash theme={null} curl -H "Authorization: Bearer $SATURDAY_API_KEY" \ https://api.saturday.fit/v1/activity-types ``` 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 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. 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. ### Using your key ```python Python theme={null} import os import requests headers = {"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"} response = requests.get("https://api.saturday.fit/v1/athletes", headers=headers) ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/athletes", { headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}` }, }); ``` ### Key management **Creating additional keys.** One key per service or deployment stage keeps a compromise contained and makes rotation a non-event. ```bash theme={null} curl -X POST https://api.saturday.fit/v1/partner/api-keys \ -H "Authorization: Bearer $SATURDAY_API_KEY" \ -H "Content-Type: application/json" \ -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 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.** Rotation mints a replacement and revokes the old key in the same call, with no grace period. The old key stops working immediately. ```bash theme={null} curl -X POST https://api.saturday.fit/v1/partner/api-keys/{key_id}/rotate \ -H "Authorization: Bearer $SATURDAY_API_KEY" ``` For a cutover with no downtime, do not rotate. Create a second key, deploy it everywhere, confirm traffic has moved, then revoke the first. **Revoking keys.** Revocation takes effect immediately. ```bash theme={null} curl -X DELETE https://api.saturday.fit/v1/partner/api-keys/{key_id} \ -H "Authorization: Bearer $SATURDAY_API_KEY" ``` ## Environments | Environment | Base URL | Data | | ----------- | ------------------------------- | ------------------- | | Production | `https://api.saturday.fit` | Real athlete data | | Sandbox | Issued with your `sk_test_` key | Synthetic test data | The key prefix and the base URL travel together. A `sk_test_` key does not authenticate against `api.saturday.fit`. ### Sandbox behavior * 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 practices 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 and authorization failures share the standard [error envelope](/error-handling): ```json theme={null} { "error": { "type": "authentication_error", "code": "invalid_api_key", "message": "Invalid API key.", "documentation_url": "https://docs.saturday.fit/errors#invalid_api_key", "request_id": "req_abc123def456" } } ``` | 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. # Become a coach Source: https://docs.saturday.fit/coaching/become-a-coach Two ways to start coaching on Saturday, and the plan you land on # Become a coach 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? 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)**. 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 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:** 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 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 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. 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 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). 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 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. 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. # Coach-paid athletes Source: https://docs.saturday.fit/coaching/billing/coach-paid-athletes Cover an athlete beyond your included seats: what it costs and how it is billed # Coach-paid athletes 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 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). 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 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. 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 From an athlete's row menu or their billing drawer on your **[roster](https://coach.saturday.fit/dashboard)**: * **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 what changes before you confirm. 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. ## At trial end When a coach-created athlete's trial ends: * **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 and stop flows. * [Who pays](/coaching/billing/who-pays), how coach-paid fits among all the modes. # Take over & relinquish Source: https://docs.saturday.fit/coaching/billing/take-over-relinquish Start covering an athlete, or hand payment back: what each side sees and what it costs # Take over & relinquish 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 Every coverage change opens a **billing-impact preview**. It tells you: * 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 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 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. 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, by choice, on a downgrade, or when you stop coaching, the athlete becomes payment-responsible: 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 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 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 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 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. # The volume discount Source: https://docs.saturday.fit/coaching/billing/volume-discount The per-athlete price drops as your coach-paid count grows: the full curve # The volume discount 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 | 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 | 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 **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 You are on **Head Coach**, which includes 5 seats, and you cover **17 athletes** in total: * 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. # Who pays Source: https://docs.saturday.fit/coaching/billing/who-pays The billing modes: self-pay, included seat, coach-paid, and org-paid # Who pays 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 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. 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 | Mode | Who pays the athlete's Saturday sub | When it applies | | ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------ | | **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. ## Included seats come first 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. ## 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 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. 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), your per-athlete price as you cover more. * [Take over & relinquish](/coaching/billing/take-over-relinquish), starting and stopping coverage. # Invite your first athlete Source: https://docs.saturday.fit/coaching/first-athlete Invite, create, or bulk-import athletes, and what happens when they accept # Invite your first athlete 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**, 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. 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 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**, 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. 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. ## Set an athlete up 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. ## Manage invites and relationships | 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 | An athlete awaiting acceptance carries a **Pending** badge on the roster. **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 from their row. See [Who pays](/coaching/billing/who-pays). * **[Message](https://coach.saturday.fit/dashboard/messages)** them, or add them to a group. A quick first pass: invite an athlete, open their fueling plan, and use **Apply this plan to Setup**. # Bottling: fill & mix Source: https://docs.saturday.fit/coaching/nutrition/bottling-fill-mix How Saturday turns per-hour targets into real bottles # Bottling: fill & mix
**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 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 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 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 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) 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. # Eco-mode vs products Source: https://docs.saturday.fit/coaching/nutrition/eco-mode-vs-products Two ways to hit the same targets: mix from staples, or use branded products # Eco-mode vs products
**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. 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. **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 gels, or can I mix my own?"* ## 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 * **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, 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) 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. # Gluc:fruc ratio Source: https://docs.saturday.fit/coaching/nutrition/gluc-fruc-ratio The balance of glucose to fructose in an athlete's carbohydrate, and when to change it # Gluc:fruc ratio
**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. **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 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. **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. **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). * **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) 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. # The dials: how Saturday thinks about fueling Source: https://docs.saturday.fit/coaching/nutrition/overview The model behind an athlete's fueling plan, and the dials you turn to change it # The dials 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 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. Which sugars make up the carbohydrate. How far to lean on the kitchen versus branded products. Where all three live, and how you adjust them. ## The short version | Dial | In one line | | -------------------- | ---------------------------------------------------------------------------------- | | 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](/coaching/overview) for how coach and athlete share control across the portal. # The Setup Source: https://docs.saturday.fit/coaching/nutrition/setup-dials Where an athlete's dials live, how you adjust them, and what the athlete sees # The Setup
**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 open it.** Any time you onboard, review, or troubleshoot an athlete's fueling. **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?"* ## 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. 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 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 * **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) 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) # Coaching on Saturday Source: https://docs.saturday.fit/coaching/overview The coach portal: billing, roster, alerts, your team, and the fueling itself # Coaching on Saturday 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 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 an empty roster to a coached athlete. The four billing modes and how they combine. How Saturday turns targets into bottles. ## What you can do | 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. 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 (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 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. 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. Open your **[dashboard](https://coach.saturday.fit/dashboard)** to follow along. # The AI report Source: https://docs.saturday.fit/coaching/roster/ai-report A plain-language read on each athlete's fueling, computed from their own session numbers # The AI report 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, 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 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 From the report you can: * **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. 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. 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. ## Coach-facing metrics 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) 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. # Configuring alerts Source: https://docs.saturday.fit/coaching/roster/configuring-alerts Tell Saturday when to ping you, by concern, threshold, urgency, and channel # Configuring alerts 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 refine. | Preset | Behavior | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **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 Rules resolve at three scopes, layered from broadest to narrowest: * **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 are watching closely. 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. ## What you can configure * **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. **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 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) 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. # Flags & groups Source: https://docs.saturday.fit/coaching/roster/flags-and-groups See who needs attention at a glance, and organize your roster into groups # Flags & groups 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 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". 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. 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 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. 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 | Action | What it does | | ------------------------- | -------------------------------------------- | | 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 | 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) 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. # Add assistants Source: https://docs.saturday.fit/coaching/team/add-assistants Bring assistant coaches onto your team and split the roster load # Add assistants 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). ## Assistants included with each plan | Plan | Assistants included | | ------------ | ------------------- | | Coach (free) | None | | Pro Coach | 1 | | Head Coach | 5 | | Business | Unlimited | | Enterprise | Unlimited | 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**, choose **Invite assistant** and enter their email. When they accept and activate, they appear on your team and can be assigned 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, and message the athlete. What an assistant cannot do by default: * 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. ## Manage your team | Action | What it does | | ------------------------- | ----------------------------------- | | Resend invite | Re-send a pending assistant invite | | 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. 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) 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. # Assign athletes Source: https://docs.saturday.fit/coaching/team/assign-athletes Give each assistant the right athletes, one at a time, in bulk, or by group # Assign 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, 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). ## 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 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 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. Do this from your [roster](https://coach.saturday.fit/dashboard). ## Head coach visibility 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) 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. # Permissions Source: https://docs.saturday.fit/coaching/team/permissions Roles, scopes, and access control across your team and organization # Permissions 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. ## Roles and scopes A permission is never granted in the abstract. Every grant carries a **scope** that decides how much data it covers: | 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 | 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. ## Preset roles The built-in roles you can assign on [Access & Roles](https://coach.saturday.fit/admin/rbac): | 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 | | --------------- | ------------------------------------------------------------------------------- | | 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, 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, audit, and 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) 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. # The team roll-up Source: https://docs.saturday.fit/coaching/team/team-rollup See how your whole operation is doing, per athlete, per coach, and in aggregate # The team roll-up 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. 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 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 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. ## Coach-facing only 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) 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. # Error Handling Source: https://docs.saturday.fit/error-handling Error format, types, codes, and retry logic # Error Handling Saturday uses one error format across all endpoints, modeled on Stripe's error structure. ## Error format ```json theme={null} { "error": { "type": "invalid_request", "code": "missing_field", "message": "activity_type is required", "param": "activity_type", "documentation_url": "https://docs.saturday.fit/errors#missing_field", "request_id": "req_abc123def456" } } ``` | Field | Always present | Description | | ------------------- | -------------- | ---------------------------------------------------------- | | `type` | Yes | Category of error | | `code` | Yes | Specific error code, machine-readable | | `message` | Yes | Human-readable explanation | | `param` | No | The request parameter that caused the error | | `documentation_url` | Yes | Docs link for this code | | `request_id` | Yes | Identifier for this request. Include it in support tickets | ## Error types 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 | | `not_found_error` | 404 | The URL does not match an endpoint available to your key | | `resource_not_found` | 404 | The endpoint exists, but the referenced resource doesn't, 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) | A 404 carries one of two types. `not_found_error` means the URL itself found no endpoint (including endpoints not available to your key), always with `code: "resource_not_found"`. `resource_not_found` as a type comes from an endpoint that could not find the thing you referenced, and its `code` narrows it: `athlete_not_found`, `activity_not_found`, `key_not_found`, `webhook_not_found`, `conversation_not_found`. Handle both types as the same case unless you need the distinction. 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 | Type | Retry? | Strategy | | ---------------------- | ------ | ------------------------------------------------------------------------------------------ | | `authentication_error` | No | Fix credentials | | `authorization_error` | No | Fix permissions, scopes, or tier | | `invalid_request` | No | Fix the request | | `not_found_error` | No | Check the URL and your key's feature access | | `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 `Retry-After` is absent on some 429s, notably the daily call ceiling, so read it with a fallback rather than indexing it directly. ```python Python theme={null} import time import random def request_with_retry(make_request, max_retries=3): for attempt in range(max_retries + 1): response = make_request() if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 60)) time.sleep(retry_after) continue if response.status_code >= 500: if attempt < max_retries: delay = (2 ** attempt) + random.uniform(0, 1) time.sleep(delay) continue return response return response ``` ```typescript TypeScript theme={null} async function requestWithRetry( makeRequest: () => Promise, maxRetries = 3 ): Promise { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await makeRequest(); if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") ?? "60"); await new Promise((r) => setTimeout(r, retryAfter * 1000)); continue; } if (response.status >= 500 && attempt < maxRetries) { const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000; await new Promise((r) => setTimeout(r, delay)); continue; } return response; } throw new Error("Max retries exceeded"); } ``` ### Request IDs 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. # Activities Source: https://docs.saturday.fit/guides/activities Creating activities, prescribing for them, and recording how they went # Activities 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. Whatever your platform calls these (workouts, sessions, events), the Saturday object is an activity, and every field and route below uses that word. ## Activity lifecycle 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 theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/athletes/ath_abc123/activities", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "type": "run", "duration_min": 120, "intensity_level": 5, "thermal_stress_level": 4, "external_id": "your-activity-88213", }, ) activity = response.json() print(f"Activity ID: {activity['id']}") ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/athletes/ath_abc123/activities", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ type: "run", duration_min: 120, intensity_level: 5, thermal_stress_level: 4, external_id: "your-activity-88213", }), } ); const activity = await response.json(); console.log(`Activity ID: ${activity.id}`); ``` ### Activity fields | Field | Type | Required | Description | | ---------------------- | ------- | -------- | --------------------------------------------------------------------------------------------- | | `type` | string | Yes | Activity type (see [supported types](/guides/nutrition-calculation#supported-activity-types)) | | `duration_min` | integer | Yes | Expected duration in minutes | | `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 | | `meal_before_min` | integer | No | Minutes between the athlete's last meal and the start | | `external_id` | string | No | Your platform's activity ID | 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`. `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. ### 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. To work out which legs a multi-sport title implies, use `POST /v1/infer/brick-types`. ## Calculating a prescription ```bash theme={null} POST /v1/athletes/{athlete_id}/activities/{activity_id}/calculate ``` 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 `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 included on every tier. ## Getting a stored prescription ```bash theme={null} GET /v1/athletes/{athlete_id}/activities/{activity_id}/prescription ``` Returns the most recently calculated prescription for this activity, wrapped with safety metadata: ```json theme={null} { "prescription": { "carb_g_per_hr": 62.5, "sodium_mg_per_hr": 485.0, "fluid_ml_per_hr": 620.0, "total_carb_g": 125, "total_sodium_mg": 970, "total_fluid_ml": 1240, "calculated_at": 1737010500 }, "safety": { "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 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 theme={null} GET /v1/athletes/{athlete_id}/activities/{activity_id} ``` The activity response carries the stored prescription inline. ## Listing activities ```bash theme={null} GET /v1/athletes/{athlete_id}/activities?limit=50&cursor=1737043200 ``` 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 Record how the fueling went: ```python Python theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/athletes/ath_abc123/activities/act_xyz789/feedback", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "rating": 4, "notes": "Felt good until mile 18, then energy dropped", }, ) ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/athletes/ath_abc123/activities/act_xyz789/feedback", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ rating: 4, notes: "Felt good until mile 18, then energy dropped", }), } ); ``` ### Feedback fields | Field | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------------------- | | `rating` | integer | Yes | Overall prescription quality, 1 (poor) to 5 (excellent) | | `notes` | string | No | Free-text feedback on how the fueling went | 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 one: ```bash theme={null} POST /v1/infer/activity-type ``` ```json theme={null} { "title": "Morning jog around Green Lake", "description": "Easy recovery after yesterday's intervals", "tags": ["recovery", "outdoor"] } ``` Response: ```json theme={null} { "type": "run", "confidence": 0.95 } ``` `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. # The Athlete Onboarding Journey Source: https://docs.saturday.fit/guides/athlete-onboarding-journey Narrative walkthrough of what an athlete sees and what your code should do, step by step 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 An athlete on your platform is always in exactly one of these states: | State | What calculations return | What moves them forward | | --------------------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------- | | **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 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`. **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 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 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 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 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 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 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 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 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". 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` and `fueling_path_copy` once. 3. Handle `athlete.profile_completed`, if you want to mark the moment in your UI. Saturday handles the questions, the hosted page, the linking, and the checkout. # Athletes Source: https://docs.saturday.fit/guides/athletes Managing athlete profiles for personalized nutrition # Athletes 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 reach athletes created through your API key. There is no cross-partner data access. ## Creating an athlete ```python Python theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/athletes", 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, "settings": {"sweat_level": 7, "saltiness": 5}, }, ) athlete = response.json() print(f"Saturday ID: {athlete['id']}") ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/athletes", { method: "POST", headers: { 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, settings: { sweat_level: 7, saltiness: 5 }, }), }); const athlete = await response.json(); 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 athletes up without storing Saturday's own IDs. ## Profile fields 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). ### Top-level fields | Field | Type | Description | | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------- | | `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 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. ## Listing athletes ```bash theme={null} GET /v1/athletes?limit=50&cursor=1737043200 ``` 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 ```bash theme={null} PATCH /v1/athletes/{id} ``` Include only the top-level fields you want to change. Unspecified top-level fields are not modified. ```python Python theme={null} import os import requests response = requests.patch( "https://api.saturday.fit/v1/athletes/ath_abc123def456", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={"weight_kg": 72, "email": "alex@example.com"}, ) ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/athletes/ath_abc123def456", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ weight_kg: 72, email: "alex@example.com" }), } ); ``` ### Updating the fueling profile ```bash theme={null} 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 theme={null} DELETE /v1/athletes/{id} ``` Removes the athlete document only. Their activities, prescriptions, and feedback stay in storage. ```bash theme={null} 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. 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 held for an athlete: ```bash theme={null} POST /v1/athletes/{id}/export ``` Returns JSON containing the athlete's profile, every activity with its prescription and feedback, any AI coach conversations, and their consent records. ## Settings schema Fetch the global settings schema to build dynamic forms: ```bash theme={null} GET /v1/settings/schema ``` 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`. 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). # Brand & Attribution Source: https://docs.saturday.fit/guides/attribution When to show the Saturday mark, where it goes, and how it should look # Brand & Attribution Building with Saturday is welcome, and we want athletes to know when Saturday's nutrition intelligence is behind what they see. Attribution is how that works. It credits the engine doing the fueling math, keeps the athlete's picture honest about where the numbers come from, and gives every partner surface a path back to Saturday for athletes who want full precision. This page is the single source for what to display, where, and how. The one-line requirement also lives in the [API terms](/guides/data-policy); the pixels live here so they can improve without reopening an agreement. ## When attribution is required Saturday's freemium model sets the line. See [Freemium Model](/guides/freemium-model) for how teaser and full responses differ. | Your situation | Attribution | | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **Teaser data** (free ranges) shown to any user | **Required.** The "Powered by Saturday" mark, linking out, near the data. | | **Full data** (subscribed athlete, exact numbers) | **Required, lighter.** A compact "Powered by Saturday" mark wherever Saturday data appears. Placement is yours; presence is not optional. | | **Personal, single-account use** (your own data, no third-party users) | The marks are welcome and appreciated. No pre-launch review. | **Safety metadata is never gated and never stripped.** Whatever tier you show, the safety fields ship with it. Attribution rules govern the brand mark, not the safety data. ## What to display Refer to Saturday one of two ways, and no other: * **"Powered by Saturday"** wherever Saturday nutrition data appears. * **"Compatible with Saturday"** when you describe interoperability in prose without rendering data. 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, 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. 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. ## Where it goes Attribution sits **with the data it credits**, visible without interaction. * Place it directly beside or beneath the nutrition figures, above the fold, visually tied to the numbers it supports. * Never bury it in a tooltip, footnote, settings screen, "info" modal, or a collapsed or expandable panel. If the athlete can see Saturday's numbers, they can see the mark without tapping anything. * For a list or feed of many entries, you may attribute once in a section header or footer, or once per entry. Either is fine as long as the mark travels with the data. Scope it by surface: | Surface | What to show | | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Summary card / feed / overview** | "Powered by Saturday" beside the fuel, hydration, or sodium figures. | | **Detail view / report / history** | The same mark, once in the header or per entry. | | **Exports and downstream transfer** (CSV, PDF, another API, a webhook you forward) | Keep the attribution adjacent to the data and repeated per page. If you pass Saturday data to another system, carry the requirement forward in your own terms. | | **Derived or blended output** (Saturday data fed into your analytics, a model, or merged with other sources) | Name Saturday as a contributing data source. Do not imply Saturday endorses the combined result. | | **Shared images / social** (a card or infographic a user exports) | The mark stays visible in the image. | When your integration also relays a third party's data alongside Saturday's (for example a wearable's device data), honor that source's attribution too. Name the origin, not only the vendor. ## How it should look 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 * **Minimum size:** stacked, 32px tall on screen (40px or larger recommended), 12mm in print; horizontal, 16px tall on screen (20px or larger recommended), 6mm in print. Below that, use 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:** 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. 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. ### App icon for integration tiles When your surface lists Saturday as an app or integration (an app directory, an integrations list, a connected-services tile), use the Saturday app icon rather than the attribution marks. The [brand kit](https://saturday.fit/brand/app-icon/) ships it at eight sizes, 24px through 1024px, in two shapes. Prefer the `squircle` files, pre-masked to the iOS-style continuous-corner shape: use that shape unless your platform already applies its own icon mask or your design system has a strong style reason for another one. The full-bleed square files exist for platforms that mask icons themselves. Use 48px or larger where the surface allows (24px is the floor), and do not recolor, crop, re-mask, or add effects. The icon identifies the Saturday app; the marks credit 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 `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 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. ## Example Both examples sit on a light background, so both use the `-light` variant. Swap to `-dark` on a dark surface. ```html Web theme={null}

Carbohydrates: 60-80 g/hr

Hydration: 600-900 mL/hr

Sodium: 300-600 mg/hr

Powered by Saturday
``` ```dart Flutter theme={null} Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Carbohydrates: 60-80 g/hr'), Text('Hydration: 600-900 mL/hr'), Text('Sodium: 300-600 mg/hr'), GestureDetector( onTap: () => launchUrl(Uri.parse(subscribeUrl)), child: SvgPicture.asset( 'assets/brand/powered-by-saturday-stacked-light.svg', height: 40, ), ), ], ) ```
## Brand assets The "Powered by Saturday" and "Compatible with Saturday" marks (light and dark, stacked and horizontal, SVG and PNG) plus ready-to-paste HTML and Flutter snippets. Download the kit at saturday.fit/brand, or email [api@saturday.fit](mailto:api@saturday.fit) and we will send it. ## Reviews and changes Saturday may review an integration for attribution compliance and ask for changes. Reach [api@saturday.fit](mailto:api@saturday.fit) any time and we will help you get it right. Unresolved noncompliance may lead to suspension or termination of API access. Saturday may update these guidelines; the current version here governs. # Batch Operations Source: https://docs.saturday.fit/guides/batch-operations Batch calculations, bulk athlete creation, and activity imports # Batch Operations 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. ```bash theme={null} 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 theme={null} 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": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={"scenarios": [dict(s, athlete_id=ATHLETE) for s in week]}, ) 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 theme={null} 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 ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ scenarios: week.map((s) => ({ ...s, athlete_id: ATHLETE })), }), } ); const data = await response.json(); data.results.forEach((r: any, i: number) => console.log(`scenario ${i}: ${r.carb_g_per_hr}g carbs/hr`) ); ``` ### 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 theme={null} { "results": [ { "tier": "full", "carb_g_per_hr": 30, "sodium_mg_per_hr": 300, "fluid_ml_per_hr": 400, "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 } }, { "tier": "full", "carb_g_per_hr": 55, "sodium_mg_per_hr": 400, "fluid_ml_per_hr": 500, "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": [], "total": 3, "succeeded": 3, "failed": 0, "estimated_ms": 4200, "elapsed_ms": 4381, "request_id": "req_ca5a6125aad2" } ``` 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 | Constraint | Value | | ----------------------- | --------------------------------- | | 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. ```bash theme={null} 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 theme={null} import requests import os response = requests.post( "https://api.saturday.fit/v1/athletes/batch", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "athletes": [ { "external_id": "user-001", "name": "Alice Runner", "weight_kg": 58, "fitness_level": "advanced", "primary_sport": "run", }, { "external_id": "user-002", "name": "Bob Cyclist", "weight_kg": 75, "fitness_level": "intermediate", "primary_sport": "bike", }, { "external_id": "user-003", "name": "Carol Triathlete", "weight_kg": 65, "fitness_level": "elite", "primary_sport": "bike", }, ], }, ) results = response.json() for athlete in results["created"]: print(f"Created: {athlete['name']} -> {athlete['id']}") ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/athletes/batch", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ athletes: [ { external_id: "user-001", name: "Alice Runner", weight_kg: 58, fitness_level: "advanced", primary_sport: "run", }, { external_id: "user-002", name: "Bob Cyclist", weight_kg: 75, fitness_level: "intermediate", primary_sport: "bike", }, ], }), }); const results = await response.json(); results.created.forEach((a: any) => console.log(`Created: ${a.name} -> ${a.id}`)); ``` ### Limits | Constraint | Value | | ---------------------- | ------------------------------------------------- | | Max athletes per batch | 100 | | 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 Import multiple activities for an athlete at once. Useful for backfilling historical data from other platforms. ```bash theme={null} 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 theme={null} import requests import os response = requests.post( "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", "duration_min": 48, "intensity_level": 7, "thermal_stress_level": 2, "is_race_event": True, "external_id": "strava-10k-2025-01-01", }, { "type": "bike", "duration_min": 150, "intensity_level": 5, "external_id": "strava-group-ride-2025-01-04", }, ], }, ) ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/athletes/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d/activities/import", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ activities: [ { type: "run", duration_min: 48, intensity_level: 7, thermal_stress_level: 2, is_race_event: true, external_id: "strava-10k-2025-01-01", }, { type: "bike", duration_min: 150, intensity_level: 5, external_id: "strava-group-ride-2025-01-04", }, ], }), } ); ``` 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: ```json theme={null} { "calculate": true, "activities": [ { "type": "run", "duration_min": 48 }, { "type": "bike", "duration_min": 150, "calculate": true } ] } ``` 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, 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 count against rate limits per item, like batch calculate. ## Error handling in batch operations 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. 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 theme={null} { "created": [ { "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, "code": "invalid_value", "message": "weight_kg must be positive" } ], "total": 3, "succeeded": 2, "failed": 1, "request_id": "req_ca5a6125aad2" } ``` 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. # Coach API Source: https://docs.saturday.fit/guides/coach-api Read your roster's fueling data and configure alerts, reports, and webhooks # Coach API The Coach API exposes a coach's Saturday surface over a coach-scoped REST API, and over an [MCP connector](/guides/coach-connector): the per-athlete fueling rollup, AI reports, the roster's needs-attention markers, and the notification and alerting settings. It is built for a coach who wants to automate fueling monitoring: read who needs attention, pull a report, and configure alert rules and digests across a whole roster programmatically. **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 Two ways to authenticate as a coach. Both resolve to the same coach identity and roster. | Method | Token | Best for | | ------------------------- | ---------------------------------- | --------------------------------------------- | | **Coach API key** | `cp_live_…` / `cp_test_…` (Bearer) | Scripts, automations, server-to-server | | **OAuth2 (coach scopes)** | OAuth2 access token (Bearer) | The Claude.ai connector / user-delegated apps | Coach API keys are minted in the [coach portal](https://coach.saturday.fit) under **[Admin → API Keys](https://coach.saturday.fit/admin/api-keys)**. Pass the key as a Bearer token: ```bash theme={null} curl -H "Authorization: Bearer cp_live_..." https://api.saturday.fit/v1/coach/roster ``` OAuth2 coaches use the [connector OAuth flow](/guides/oauth2) with coach scopes (`coach:roster`, `coach:reports`, `coach:alerts`, `coach:webhooks`). Because a coach is also an athlete, the same OAuth token acts as an athlete-self token on `/v1/athletes/*`. ## Scopes and capabilities The coach's token or key carries scopes that map to capabilities. A route is reachable only when the token confers the matching capability; otherwise it returns the uniform `404`. | Capability | OAuth scope | API-key scope | Grants | | -------------------------------- | ---------------- | ------------------------------------------------------ | ---------------------------------------------------------- | | Read roster + rollups + sessions | `coach:roster` | `roster:read`, `roster:write`, `org:read`, `org:write` | `GET /v1/coach/roster`, `…/fueling-rollup`, `…/sessions/…` | | Read reports + digest | `coach:reports` | `org:read`, `org:write` | `…/report`, `/v1/coach/roster/digest` | | Write alert + report config | `coach:alerts` | `roster:write`, `org:write` | `/v1/coach/config/*` | | Manage webhooks | `coach:webhooks` | `webhooks:manage` | `/v1/coach/webhooks/*` | The two vocabularies are unioned, never translated, so a key keeps exactly the reach its scopes confer. `roster:read` alone does not open the report routes; pick `org:read` or higher when a key needs reports. Keys minted before scoped keys existed carry a single `*` scope and hold all four capabilities. **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` returns every athlete you coach, each with a needs-attention summary over the look-back window. ```bash theme={null} curl -H "Authorization: Bearer cp_live_..." \ "https://api.saturday.fit/v1/coach/roster?window=14" ``` | Query param | Values | Default | | ----------- | --------------- | ----------------------------- | | `window` | `7`, `14`, `30` | your configured report window | ```json theme={null} { "coach_uid": "coach_abc", "window": 14, "athletes": [ { "athlete_uid": "ath_123", "flagged": true, "flagged_count": 2, "top_reasons": ["Sodium 57%", "1 symptom"], "session_count": 6 } ] } ``` The markers come from Saturday's single shared concern definition, so they match the coach portal table and the digest exactly. An athlete who fueled well shows `flagged: false`. ### Roster digest (flagged-only) `GET /v1/coach/roster/digest` returns the same data for athletes who crossed a concern bar this window, most-flagged first. Athletes who did not cross a bar are omitted, which is what makes this the useful call on a large roster. ```json theme={null} { "coach_uid": "coach_abc", "window": 7, "flagged_count": 3, "total_count": 48, "flagged": [ { "athlete_uid": "ath_123", "flagged": true, "flagged_count": 2, "top_reasons": ["Sodium 57%", "1 symptom"], "session_count": 6 } ] } ``` `flagged` carries the same entry shape as `GET /v1/coach/roster` above, ordered most-flagged first. `flagged_count` counts the entries in it; `total_count` counts the whole roster. ### Per-athlete fueling rollup `GET /v1/coach/athletes/{athlete_uid}/fueling-rollup` returns one athlete's in-window sessions (the same table the portal renders) plus the concern summary and the resolved cutoffs that produced the markers. | Query param | Values | Default | | ----------- | ------------------------- | ---------------------- | | `window` | `7`, `14`, `30` | your configured window | | `focus` | `worst`, `rolling`, `key` | your configured focus | ```json theme={null} { "athlete_uid": "ath_123", "window": 14, "focus": "rolling", "sessions": [ { "activity_id": "act_789", "date_millis": 1749480000000, "type": "ride", "duration_min": 210, "is_race": false, "carb_pct": 0.82, "sodium_pct": 0.57, "fluid_pct": 0.94, "consumed_carb_g": 172, "consumed_sodium_mg": 1140, "consumed_fluid_ml": 2350, "user_rating": 3, "symptoms": { "cramp": 1 }, "sleep_hours": 6.5 } ], "concern": { "flagged": true, "flagged_count": 2, "top_reasons": ["Sodium 57%", "1 symptom"] }, "settings_resolved": { "report_window_days": 14, "report_focus": "rolling", "concern_carb_cutoff": 0.7, "concern_sodium_cutoff": 0.7, "concern_fluid_cutoff": 0.7, "hyponatremia_fluid_min": 0.9, "hyponatremia_sodium_max": 0.5 } } ``` The session object above is abbreviated. Each entry also carries the as-used and suggested prescription totals, prep fidelity, leftover reuse, report source and completeness, intensity, the profile snapshot, opaque `vessel_reports` and `weather` maps, and the derived `adherence_vs_suggested`, `dial_down_gap`, and `per_hour` triples. The adherence fractions are uncapped, so a value above `1.0` is genuine over-consumption rather than an error. Every missing value stays `null`, never `0`, and an absent `symptoms` key is not a zero. ### AI report `GET /v1/coach/athletes/{athlete_uid}/report` returns the AI-generated fueling report: a third-person narrative grounded only in the athlete's own numbers, plus the structured concern summary behind it, so an agent can quote the prose or compute on the data. | Query param | Values | Notes | | ----------- | ------------------------- | ------------------------------------------------------- | | `window` | `7`, `14`, `30` | look-back | | `focus` | `worst`, `rolling`, `key` | report focus | | `refresh` | `true` | force regeneration even if a cached report exists | | `format` | `pdf` | return the report as a downloadable PDF instead of JSON | ```json theme={null} { "athlete_uid": "ath_123", "window": 14, "focus": "rolling", "narrative": "Over the last two weeks, this athlete …", "concern": { "flagged": true, "flagged_count": 2, "top_reasons": ["Sodium 57%", "1 symptom"] }, "generated_at": 1749500000000, "latest_session_ms": 1749480000000, "from_cache": true } ``` Served from cache unless a newer session has landed or `refresh=true`. ### Session detail `GET /v1/coach/athletes/{athlete_uid}/sessions/{activity_id}` drills into one session: the full per-session projection (planned-vs-actual fueling, symptoms, vessel reports, weather, sleep) plus the concern markers that session crossed. ## Configuration Most of the Coach API is a configuration surface. Anything a coach can configure in the portal is configurable here: channels, triggers, per-nutrient thresholds, the overall/group/athlete scope hierarchy, consolidation, cadence, quiet hours, and AI-report defaults. The portal UI and the API are two views of one config model. **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. `PUT /v1/coach/config/notification-rules` replaces the rule set at a scope. It is an idempotent upsert: re-running with the same body is a no-op, so no per-request idempotency key is needed. ```bash theme={null} curl -X PUT https://api.saturday.fit/v1/coach/config/notification-rules \ -H "Authorization: Bearer cp_live_..." \ -H "Content-Type: application/json" \ -d '{ "scope": "group", "scope_id": "grp_elite", "rules": { "notification_rules": { "under_fuel": { "enabled": true, "channels": ["email", "webhook"], "cadence": "realtime", "urgent_threshold": 0.6 }, "hyponatremia_pattern": { "enabled": true, "channels": ["push"], "cadence": "realtime" } }, "combinators": [ { "trigger_a": "under_fuel", "trigger_b": "symptom", "channel": "push", "cadence": "realtime" } ], "quiet_hours": { "enabled": true, "start": "22:00", "end": "06:00", "tz": "America/Denver" } } }' ``` **Triggers:** `under_fuel`, `symptom`, `low_rating`, `hyponatremia_pattern`, `dial_down`, `sleep_trend`, `went_quiet`. **Channels:** `in_portal`, `email`, `push`, `webhook`. (SMS is not yet supported.) **Cadence:** `realtime` (sent as it happens) or `digest` (bundled into one daily item per athlete). **Thresholds** are fractions in `(0,1]`. `urgent_threshold` is the urgent band. `amber_threshold` is optional and decouples this trigger's marker line from the shared cutoff; omit it to use the resolved concern cutoff. **Combinators** are bounded two-trigger ANDs: both legs must be markers on the same session ("sodium short AND a cramp"). Exactly two triggers, with no OR, NOT, or nesting. ### Presets `POST /v1/coach/config/preset` applies a named starting point at a scope, which you can then tweak rule by rule. ```bash theme={null} curl -X POST https://api.saturday.fit/v1/coach/config/preset \ -H "Authorization: Bearer cp_live_..." \ -H "Content-Type: application/json" \ -d '{ "scope": "overall", "preset": "balanced" }' ``` | Preset | Behavior | | ----------- | ------------------------------------ | | `hands_off` | in-portal only, no pings | | `balanced` | core concerns on email, daily digest | | `hands_on` | all triggers, real-time urgent push | ### AI-report & concern settings `GET` / `PUT /v1/coach/config/report-settings` reads/upserts the report window/focus defaults and any overridden concern cutoffs at a scope. Unset fields fall through to the broader scope. ```bash theme={null} curl -X PUT https://api.saturday.fit/v1/coach/config/report-settings \ -H "Authorization: Bearer cp_live_..." \ -H "Content-Type: application/json" \ -d '{ "scope": "athlete", "scope_id": "ath_123", "settings": { "ai_report_window_days": 30, "ai_report_focus": "key", "concern_sodium_cutoff": 0.65 } }' ``` **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 another delivery channel on the alerts you already configure (`"channels": ["webhook"]`). Register an endpoint, then select `webhook` as a channel on any rule. 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 theme={null} curl -X POST https://api.saturday.fit/v1/coach/webhooks \ -H "Authorization: Bearer cp_live_..." \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-system.example.com/saturday", "events": ["concern.detected", "athlete.needs_attention"] }' ``` ```json theme={null} { "id": "wh_abc", "coach_uid": "coach_abc", "url": "https://my-system.example.com/saturday", "events": ["concern.detected", "athlete.needs_attention"], "active": true, "created_at": 1749500000000, "secret": "whsec_…" } ``` `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) | | `POST /v1/coach/webhooks` | register (secret returned once) | | `DELETE /v1/coach/webhooks/{id}` | delete an endpoint | | `POST /v1/coach/webhooks/{id}/disable` | disable (stop delivery, keep the endpoint) | | `POST /v1/coach/webhooks/{id}/enable` | re-enable | **Event types:** `concern.detected` and `athlete.needs_attention`, which an empty `events` list subscribes you to, plus `coach.message.sent`, which is opt-in and only fires for Enterprise coaches. The URL must be public `https`; internal, metadata, and loopback URLs are rejected at registration. Deliveries are signed with HMAC-SHA256 in the `X-Saturday-Signature` header using the secret returned at registration, retried with backoff, and auto-disabled after 15 consecutive failures. See [Webhooks](/guides/webhooks) for verifying signatures. ## Full identity Coach API responses carry full athlete identity: names and data exactly as the coach sees them in the portal. The coach owns the coaching relationship, so the API has no de-identification layer (the export feature's identity levels are a portal and export concern). Handling that PII downstream is your responsibility. See the [data policy](/guides/data-policy). ## Rate limits Coach principals get 50 requests per second sustained with a burst of 500, so a 500-athlete roster pull or digest completes in one turn without paging penalties. These limits are set for abuse protection rather than capacity management. See [rate limiting](/rate-limiting). ## SDKs Both the [TypeScript](https://github.com/SaturdayInc/saturday-node) and [Python](https://github.com/SaturdayInc/saturday-python) SDKs expose the coach surface as a `coach` resource: ```typescript TypeScript theme={null} import Saturday from '@saturdayinc/sdk'; const saturday = new Saturday({ apiKey: 'cp_live_...' }); const digest = await saturday.coach.rosterDigest({ window: 7 }); const report = await saturday.coach.report('ath_123', { window: 14 }); await saturday.coach.applyPreset({ scope: 'overall', preset: 'balanced' }); const wh = await saturday.coach.registerWebhook('https://my-system.example.com/saturday'); // wh.secret is returned once; store it now ``` ```python Python theme={null} from saturday import Saturday client = Saturday(api_key="cp_live_...") digest = client.coach.roster_digest(window=7) report = client.coach.report("ath_123", window=14) client.coach.apply_preset(scope="overall", preset="balanced") wh = client.coach.register_webhook("https://my-system.example.com/saturday") # wh["secret"] is returned once; store it now ``` # Claude Connector Source: https://docs.saturday.fit/guides/coach-connector Connect Saturday to Claude: one connector, athlete and coach tools # Saturday Claude Connector 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** (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. ## Connect In Claude, add a custom connector pointing at: ``` 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 "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 and protocol 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 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 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). ## Coach tools 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 | Tool | Args | Returns | | ---------------------------- | --------------------------------------------- | ------------------------------------------------------- | | `get_roster` | `window?` | the roster + per-athlete needs-attention markers | | `get_roster_digest` | `window?` | flagged-only digest, most-flagged first | | `get_athlete_fueling_rollup` | `athlete_id`, `window?`, `focus?` | in-window sessions + concern summary + resolved cutoffs | | `get_athlete_report` | `athlete_id`, `window?`, `focus?`, `refresh?` | narrative + structured report | | `get_session_detail` | `athlete_id`, `activity_id` | one session's full projection + markers | `window` is one of `7`, `14`, `30`; `focus` is `worst`, `rolling`, or `key`. ### Config tools | 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) | | `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` | 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), and the most specific scope wins. `scope_id` is required for `group` and `athlete`. 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. 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 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" }` 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" }`, then store the returned secret. ## Lapsed coach 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) 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. # Data Policy Source: https://docs.saturday.fit/guides/data-policy License terms, usage restrictions, attribution, and privacy # Data Policy 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 Saturday grants partners a limited license to use API response data. This license is communicated programmatically via the `X-Saturday-Data-License` response header on every API response. ### Permitted uses Partners may use API response data for: * **Displaying** prescriptions and safety metadata to end users (athletes) * **Storing** response data temporarily to serve your application's UX * **Caching** prescriptions for performance (cache invalidation on profile update) ### Prohibited uses Partners may not use API response data for: * **Training** machine learning or AI models * **Fine-tuning** language models or other generative AI systems * **Building** competing nutrition prescription algorithms * **Aggregating** response data across athletes for statistical analysis or resale * **Reverse engineering** Saturday's calculation algorithms * **Redistributing** response data to third parties * **Creating** derivative databases from product catalog data 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 | 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` | 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 ### Teaser tier (required) When displaying teaser/free response data, partners must show "Powered by Saturday" attribution: * **Text**: "Powered by Saturday" visible near nutrition data * **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 Teaser responses carry `X-Saturday-Data-Attribution: required`, and repeat the same terms in the response body's `attribution` object. ### Full tier (required, lighter) When displaying full data for subscribed athletes, show a compact "Powered by Saturday" mark wherever Saturday data appears. Placement is yours; presence is not optional. ### Logo and brand assets Approved attribution assets (the "Powered by Saturday" mark, light and dark, horizontal and stacked, plus paste-ready snippets) live at `https://saturday.fit/brand`. Partners must not modify the Saturday mark or use Saturday's brand in misleading contexts. Full placement, sizing, and per-surface rules: [Brand & Attribution](/guides/attribution). ## 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, which are not medical advice. ### Required disclaimer language Partners must display, at minimum: > Nutrition recommendations are personalized guidance based on physiological models. They are not a substitute for professional medical evaluation. Consult a healthcare provider for medical dietary needs. ### Safety data handling Partners must NOT: * Strip, hide, or downplay safety metadata from responses * Remove safety warnings before displaying data to athletes * Override or modify Saturday's risk levels * Present prescriptions without safety context ## Athlete data and privacy ### Data controller vs. processor * **Partners** act as data controllers for their athletes' personal data * **Saturday** acts as a data processor on behalf of the partner Partners are responsible for obtaining appropriate consent from athletes before sending their data to Saturday's API. ### GDPR compliance Saturday provides endpoints for GDPR compliance: | Endpoint | Right | Description | | ------------------------------- | ---------------- | ------------------------------- | | `POST /v1/athletes/{id}/export` | Data portability | Export all athlete data as JSON | | `DELETE /v1/athletes/{id}` | Right to erasure | Cascading deletion of all data | Partners are responsible for responding to athlete data subject requests within the timelines required by applicable law. ### Data retention Saturday retains athlete data only for as long as the partner account is active, plus a 30-day grace period after deletion. After termination: * Partner must delete all cached Saturday response data within 30 days * Athletes' data remains available for GDPR export for 30 days, then is deleted ## Product database protection Saturday's curated product database (186+ endurance nutrition products) is competitive intellectual property. The following protections apply: * **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. ## Intellectual property API access does not grant any license to Saturday's intellectual property beyond the right to display response data to end users. Saturday's nutrition algorithms, product database, AI coaching models, and knowledge base are proprietary. ## Terms modifications Saturday may modify data policy terms with 30 days advance notice for standard changes or 60 days for material changes. Continued API use after the notice period constitutes acceptance. # Deployment & Configuration Source: https://docs.saturday.fit/guides/deployment Where the docs and the API live, and the machine-readable endpoints for AI agents # Deployment & Configuration Saturday runs two hostnames. This page says which is which, and lists the machine-readable endpoints an agent can fetch. ## The two hostnames | Host | What it serves | | ------------------- | ------------------------------------------- | | `docs.saturday.fit` | This documentation site, hosted on Mintlify | | `api.saturday.fit` | The API itself, including the MCP endpoint | 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 | `docs` | `cname.mintlify-dns.com` | DNS only (no proxy) | Cloudflare proxy stays **disabled** (DNS only, grey cloud) on the Mintlify CNAME. Mintlify terminates SSL and needs direct DNS resolution. 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 deploys from Git. Push to the `docs` repository's default branch and the site rebuilds, typically in under a minute. A rebuild can also be triggered by hand from [dashboard.mintlify.com](https://dashboard.mintlify.com) on the Saturday API project. ## Machine-readable endpoints ### llms.txt ``` https://docs.saturday.fit/llms.txt ``` 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 ``` https://docs.saturday.fit/llms-full.txt ``` 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 ``` https://api.saturday.fit/mcp ``` 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. ## Site 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 | | ------------------- | ------------------------------------------ | | 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/ docs.json # Site configuration and navigation llms.txt # Hand-curated LLM index introduction.mdx # Landing page 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 it is available. That hostname does not currently resolve; until it does, reach [api@saturday.fit](mailto:api@saturday.fit) about an incident. # Feature Gates Source: https://docs.saturday.fit/guides/feature-gates Launch stages, alpha access, and feature progression # Feature Gates 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. 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 | Stage | What happens | Who can access | Stability | | -------------- | --------------------------------------------------------- | ------------------------------- | ---------------------------------------------- | | **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 | 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 | Sunset date announced per feature | ### STEALTH 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 partners who have been invited. Reaching an alpha feature without being on the allowlist returns a `403`: ```json theme={null} { "error": { "type": "authorization_error", "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" } } ``` 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 theme={null} X-Saturday-Feature-Stage: ALPHA 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. Responses include: ```http theme={null} X-Saturday-Feature-Stage: BETA 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) GA features are production-stable with backward compatibility guarantees. Breaking changes go through a deprecation cycle. ### DEPRECATED Deprecated features still work but will be removed. Responses carry: ```http theme={null} X-Saturday-Feature-Stage: DEPRECATED Deprecation: true ``` 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 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) | 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 | 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 grouped so a stage transition can move several at once: | Group | Features | Example transition | | ---------- | ------------------- | ---------------------------------------------------------------- | | `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 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, 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 what your key can reach: ```bash theme={null} GET /v1/ ``` 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 | Header | When sent | Value | | -------------------------- | ----------------------- | ----------------------------------- | | `X-Saturday-Feature-Stage` | ALPHA, BETA, DEPRECATED | Stage name | | `X-Saturday-Alpha-Notice` | ALPHA (on allowlist) | Stability warning + support contact | | `X-Saturday-Beta-Notice` | BETA | Breaking changes warning | | `Deprecation` | DEPRECATED | `true` (RFC 8594) | # Freemium Model Source: https://docs.saturday.fit/guides/freemium-model Teaser vs. full responses, subscription flow, and attribution # 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 ``` ## Teaser vs. full comparison | | Teaser (free) | Full (subscribed) | | -------------------- | -------------------------------------------- | ----------------------------------- | | **Carbohydrates** | Range: `"carb_range_g_per_hr": "60-90"` | Exact: `"carb_g_per_hr": 62.5` | | **Hydration** | 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` | | **Products** | Category only ("gel") | Specific products + schedule | | **Product catalog** | Taxonomy + CTA | Search, browse, and full label data | | **Safety metadata** | Full | Full | | **Confidence score** | Shown | Shown | **Safety is never gated.** Both teaser and full responses include complete safety metadata. Safety information is always free. ## Detecting response type ```python Python theme={null} data = response.json() if data["tier"] == "teaser": # Show ranges and subscription CTA show_range(data["carb_range_g_per_hr"]) show_range(data["sodium_range_mg_per_hr"]) show_range(data["fluid_range_ml_per_hr"]) show_upgrade_cta(data["subscription_cta"]["subscribe_url"]) else: # Show exact numbers show_exact(data["carb_g_per_hr"]) show_exact(data["sodium_mg_per_hr"]) show_exact(data["fluid_ml_per_hr"]) ``` ```typescript TypeScript theme={null} const data = await response.json(); if (data.tier === "teaser") { // Show ranges and subscription CTA showRange(data.carb_range_g_per_hr); showRange(data.sodium_range_mg_per_hr); showRange(data.fluid_range_ml_per_hr); showUpgradeCTA(data.subscription_cta.subscribe_url); } else { // Show exact numbers showExact(data.carb_g_per_hr); showExact(data.sodium_mg_per_hr); showExact(data.fluid_ml_per_hr); } ``` ## The product catalog Saturday's curated product database follows the same rule as the numbers: it opens on the **athlete's** subscription, not on your partner account. Every product route (`/v1/products/search`, `/v1/products/{barcode}`, `/v1/products/curated`, `/v1/nutrition/products/fit`, `/v1/nutrition/products/compare`) takes an `athlete_id`, and the answer follows that athlete's standing: * **Subscribed, or inside their 30-day trial**: `tier: "full"`. Search and browse return products; a barcode lookup returns full label data for a curated product, or a `product_not_found` 404. * **Anyone else**: `tier: "teaser"`. The product arrays come back empty and the response carries the category taxonomy plus the same `cta` object your teaser prescriptions use. Still a 200, never a 403. ```json theme={null} { "tier": "teaser", "products": [], "total": 0, "categories": [ { "id": "gel", "name": "Gels", "description": "Energy gels and gel-like products" }, { "id": "drink_mix", "name": "Drink Mixes", "description": "Electrolyte and carb drink powders" } ], "cta": { "message": "Subscribe to see the specific products that fit this athlete, not just the categories", "subscribe_url": "https://saturday.fit/subscribe?ref=your_partner_id&pst=..." } } ``` Branch on `tier` exactly as you do for prescriptions. A teaser is a product picker that shows categories with an upgrade prompt behind them, not an error state. `GET /v1/products/categories` is the one product route that never gates: the taxonomy *is* the free tier, so it answers the same for every athlete. The catalog is stricter here than in the Saturday app, on purpose. In the app a free athlete can browse products and the subscription gates the prescription. An API has no such natural limit, so the catalog itself sits behind the athlete's subscription. Verified label data for 186+ endurance products is the asset, and it stays priced. The athlete's trial opens the catalog on its 30-day **window**, independently of the daily full-precision call cap. An athlete who has spent today's exact calculations can still see the products those calculations point at, and browsing products never debits the cap. ## 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: ```json theme={null} { "tier": "full", "tier_source": "trial", "trial_ends_at": 1768063200000, "trial_calls_remaining_today": 3, "carb_g_per_hr": 62.5 } ``` 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. ## 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 You don't handle payment — Saturday manages the subscription. Two important details: * **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"`. ### Checking entitlement `GET /v1/athletes/{id}` returns a computed `subscription_status` field — `full` | `trial` | `teaser` — for polling after checkout-return or support lookups. ### 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. ### 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. * **Opt in by supplying emails.** Set `email` on your athletes (`POST /v1/athletes` and `PATCH /v1/athletes/{athlete_id}`). 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. 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. ### 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). ### 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% ``` 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. ## Testing the loop (test environment) The test environment has a zero-payment simulator so you can integration-test your webhook handlers and tier handling end to end: ```bash theme={null} curl -X POST {TEST_BASE_URL}/v1/test/athletes/{athlete_id}/simulate-subscription \ -H "Authorization: Bearer sk_test_..." \ -H "Content-Type: application/json" \ -d '{"active": true}' ``` Your test base URL comes with your `sk_test_` key at onboarding. `{"active": true}` writes the same link fields and fires the same `subscription.created` webhook as a real purchase; `{"active": false}` fires `subscription.cancelled`. The endpoint does not exist in production. 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. ## 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. Full placement rules, marks, sizing, and the per-surface matrix live on the [Brand & Attribution](/guides/attribution) page. ## Partner value exchange * **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. # MCP Integration Source: https://docs.saturday.fit/guides/mcp-integration Consume Saturday via Model Context Protocol for AI agents # MCP Integration Saturday provides a native [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server, allowing AI agents to discover and use Saturday's nutrition intelligence tools without custom API integration code. ## 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 discovers the available nutrition tools at runtime, along with their inputs, outputs, descriptions, and safety constraints. ## Why use Saturday via MCP? | Approach | Best for | | -------------- | --------------------------------------------------------------- | | **Direct API** | Traditional server-side integrations, custom UIs | | **SDK** | TypeScript/Python applications with typed interfaces | | **MCP** | AI agents, LLM-powered platforms, automated nutrition workflows | MCP is ideal when your platform uses AI agents that need to dynamically decide when to call Saturday's tools based on conversation context. ## Connecting to Saturday's MCP server There are two ways to reach Saturday's MCP server, depending on who you are: | You are | Auth | What you get | | ----------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | A **partner** (e.g. an AI platform) | `sk_*` partner key | Your partner-scoped tools over your own `partners/{id}` athletes | | 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, with no API key. The configuration below is for **partners** integrating their platform with a partner API key. ### Server configuration (partners) Add Saturday to your MCP client configuration with your partner API key: ```json theme={null} { "mcpServers": { "saturday": { "url": "https://api.saturday.fit/mcp", "transport": "streamable-http", "headers": { "Authorization": "Bearer YOUR_PARTNER_API_KEY" } } } } ``` The server speaks the MCP **Streamable HTTP** transport and protocol revision **2025-11-25** (negotiating down to older revisions a client offers). ### Tool catalog When your agent connects, it discovers tools including: | Tool | Description | | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `calculate_nutrition` | Calculate fuel/hydration/electrolyte prescription for an activity | | `search_products` | Search the curated nutrition product database | | `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 | | `get_activity_prescription` | Get the stored prescription for an activity | | `build_bottling_plan` | Turn a prescription into a concrete, bottle-by-bottle mix plan (renders the [Bottle Builder app](#bottle-builder-interactive-mcp-app)) | | `record_bottling_choice` | Persist the athlete's chosen bottling plan | | `infer_activity_type` | Infer activity type from metadata | | `search_knowledge` | Search Saturday's sports nutrition knowledge base | **Product tools follow the athlete's subscription.** `search_products`, `list_curated_products`, and `analyze_product_fit` take an optional `athlete_id` and answer at that athlete's tier. A connection where the caller *is* the athlete (the Claude connector) resolves it from their own account and never needs to pass one; a platform integration on a partner key passes the athlete's id, and gets the category tier until it does. See [Freemium Model](/guides/freemium-model#the-product-catalog). 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 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 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 Here's how a Claude-powered agent might use Saturday's tools in a conversation: **Athlete asks:** "I have a 3-hour bike race on Saturday. It's going to be 30C and humid. What should I eat?" **Agent's tool calls:** 1. `calculate_nutrition` with `{activity_type: "bike", duration_min: 180, intensity_level: 8, is_race: true, thermal_stress_level: 8}` 2. `search_products` with `{query: "gel"}` (matched against product name, brand, type, and keywords) **Agent synthesizes:** Uses Saturday's prescription + product results + safety warnings to give a complete race-day fueling plan. ## Safety-aware tool descriptions 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. Three constraints run through them: * 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". 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 ```python Python theme={null} import os # Using the MCP Python SDK from mcp import ClientSession, StdioServerParameters async with ClientSession( StdioServerParameters( command="npx", args=["mcp-remote", "https://api.saturday.fit/mcp"], env={"SATURDAY_API_KEY": os.environ["SATURDAY_API_KEY"]}, ) ) as session: # List available tools tools = await session.list_tools() for tool in tools: print(f"{tool.name}: {tool.description}") # Call a tool result = await session.call_tool( "calculate_nutrition", { "activity_type": "run", "duration_min": 90, "intensity_level": 5, "athlete_weight_kg": 70, "thermal_stress_level": 6, }, ) print(result) ``` ```typescript TypeScript theme={null} import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const client = new Client({ name: "my-agent", version: "1.0" }); const transport = new StreamableHTTPClientTransport( new URL("https://api.saturday.fit/mcp"), { requestInit: { headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, }, }, } ); await client.connect(transport); // List available tools const tools = await client.listTools(); tools.tools.forEach((t) => console.log(`${t.name}: ${t.description}`)); // Call a tool const result = await client.callTool("calculate_nutrition", { activity_type: "run", duration_min: 90, intensity_level: 5, athlete_weight_kg: 70, thermal_stress_level: 6, }); console.log(result); ``` ## AI agent guidelines When building AI agents that consume Saturday via MCP: ### Do * **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 ### 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 ## LLM discoverability Saturday publishes machine-readable context files for AI agents: | File | URL | Purpose | | --------------- | ----------------------------------------- | --------------------------------------- | | `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 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. # Nutrition Calculation Source: https://docs.saturday.fit/guides/nutrition-calculation Personalized carbohydrate, sodium, and fluid prescriptions # Nutrition Calculation This endpoint takes an activity description and returns a fuel prescription: carbohydrate, sodium, and fluid targets, with safety guardrails applied. ```bash theme={null} POST /v1/nutrition/calculate ``` ## Progressive enrichment Saturday works with whatever data you have. More data narrows the prescription; minimal inputs still return a usable result. ### Minimal inputs The bare minimum for a calculation: ```python Python theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/nutrition/calculate", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "activity_type": "run", "duration_min": 90, "athlete_weight_kg": 70, }, ) ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/nutrition/calculate", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ activity_type: "run", duration_min: 90, athlete_weight_kg: 70, }), } ); ``` This returns a prescription with a low `confidence_score`. The numbers are based on population-level defaults for the given activity type and body weight. ### Standard inputs Add intensity and environmental conditions: ```json theme={null} { "activity_type": "bike", "duration_min": 180, "intensity_level": 7, "athlete_weight_kg": 68, "thermal_stress_level": 8 } ``` 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 For the highest accuracy, include an athlete reference: ```json theme={null} { "activity_type": "bike", "duration_min": 300, "intensity_level": 8, "is_race": true, "athlete_id": "ath_abc123", "thermal_stress_level": 7 } ``` 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 ```json theme={null} { "tier": "full", "carb_g_per_hr": 72, "sodium_mg_per_hr": 800, "fluid_ml_per_hr": 750, "total_carb_g": 360, "total_sodium_mg": 4000, "total_fluid_ml": 3750, "safety": { "max_safe_fluid_ml_per_hr": 1500, "max_safe_sodium_mg_per_hr": 3000, "confidence_score": 0.85, "requires_human_review": false, "warnings": [], "not_instructions": true }, "attribution": { "text": "Powered by Saturday", "logo_url": "https://saturday.fit/logo.png", "link": "https://saturday.fit", "required": false } } ``` ### Confidence score The `safety.confidence_score` (0.0-1.0) reports how much of the athlete's fueling profile was answered: a complete fueling profile scores 1.0, and every unanswered field lowers the score. `precision.missing_fields` tells you which answers would raise it. | 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 (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. ## Calculation timing 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`): 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. 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 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": "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, 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. Teaser responses carry the same safety block as full ones. ## Supported activity types | Type | Key | Notes | | ------------- | ------ | --------------------------------- | | Running | `run` | Road, trail, track | | Cycling | `bike` | Road, gravel, mountain | | Swimming | `swim` | Pool, open water | | Rowing | `row` | On-water, ergometer | | Skiing | `ski` | Cross-country, classic, skate | | Hiking | `hike` | Long-duration, variable intensity | | Weightlifting | `lift` | Strength training | 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. 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). # OAuth2 Source: https://docs.saturday.fit/guides/oauth2 Athlete-delegated access with PKCE authorization code flow # OAuth2 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 | Most partners start with API keys. Add OAuth2 when athletes tell you they already have Saturday accounts. **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. ## The flow Your app redirects the athlete to Saturday's authorization page Athlete logs into their Saturday account (or creates one) Athlete sees what scopes you're requesting and clicks "Allow" Saturday redirects back to your app with an authorization code Your server exchanges the code for access and refresh tokens Use the access token as a Bearer token for API requests ## Step 1: Generate PKCE challenge Saturday requires PKCE (Proof Key for Code Exchange) for all OAuth2 flows. Generate a code verifier and challenge: ```python Python theme={null} import hashlib import base64 import secrets # Generate code verifier (43-128 characters) code_verifier = secrets.token_urlsafe(32) # Generate code challenge (SHA-256 hash of verifier) 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 print(f"Verifier: {code_verifier}") print(f"Challenge: {code_challenge}") ``` ```typescript TypeScript theme={null} import crypto from "crypto"; // Generate code verifier (43-128 characters) const codeVerifier = crypto.randomBytes(32).toString("base64url"); // Generate code challenge (SHA-256 hash of verifier) const codeChallenge = crypto .createHash("sha256") .update(codeVerifier) .digest("base64url"); // Store codeVerifier in your session: you need it for token exchange console.log(`Verifier: ${codeVerifier}`); console.log(`Challenge: ${codeChallenge}`); ``` ## Step 2: Redirect to authorize Build the authorization URL and redirect the athlete: ``` https://api.saturday.fit/v1/oauth/authorize? client_id=your_partner_id &redirect_uri=https://your-app.com/callback &response_type=code &scope=athlete:read activity:read nutrition:read &state=random_csrf_token &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 ``` | Parameter | Required | Description | | ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- | | `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` | 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 After the athlete consents, Saturday redirects to your `redirect_uri` with a code: ``` https://your-app.com/callback?code=AUTH_CODE_HERE&state=random_csrf_token ``` 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: ``` https://your-app.com/callback?error=access_denied&error_description=The+user+denied+access&state=random_csrf_token ``` ## Step 4: Exchange code for tokens ```python Python theme={null} import requests response = requests.post( "https://api.saturday.fit/v1/oauth/token", data={ "grant_type": "authorization_code", "code": auth_code, "redirect_uri": "https://your-app.com/callback", "client_id": "your_partner_id", "client_secret": "your_client_secret", "code_verifier": code_verifier, }, ) tokens = response.json() access_token = tokens["access_token"] # JWT, 1-hour expiry refresh_token = tokens["refresh_token"] # Opaque, 90-day expiry expires_in = tokens["expires_in"] # Seconds until access token expires ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: authCode, redirect_uri: "https://your-app.com/callback", client_id: "your_partner_id", client_secret: "your_client_secret", code_verifier: codeVerifier, }), }); const tokens = await response.json(); const accessToken = tokens.access_token; // JWT, 1-hour expiry const refreshToken = tokens.refresh_token; // Opaque, 90-day expiry ``` ## Step 5: Use the access token The access token is a JWT that contains the partner ID, athlete UID, and granted scopes. Use it as a Bearer token: ```bash theme={null} curl -H "Authorization: Bearer ACCESS_TOKEN_JWT" \ https://api.saturday.fit/v1/nutrition/calculate \ -X POST -H "Content-Type: application/json" \ -d '{"activity_type": "bike", "duration_min": 120, "intensity_level": 3}' ``` 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 Access tokens expire after 1 hour. Use the refresh token to get a new pair: ```python Python theme={null} import requests response = requests.post( "https://api.saturday.fit/v1/oauth/token", data={ "grant_type": "refresh_token", "refresh_token": refresh_token, "client_id": "your_partner_id", }, ) new_tokens = response.json() # Rotation: save the new refresh token, the old one stops working shortly access_token = new_tokens["access_token"] refresh_token = new_tokens["refresh_token"] ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: "your_partner_id", }), }); const newTokens = await response.json(); // Rotation: save the new refresh token, the old one stops working shortly ``` **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 | Scope | Access granted | | ---------------- | ------------------------------------- | | `athlete:read` | View athlete profile and settings | | `athlete:write` | Modify athlete profile and settings | | `activity:read` | View activities and prescriptions | | `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 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 | | ---------------- | ------------------------------------------------------------------------ | | `coach:roster` | View the coach's athlete roster and per-athlete needs-attention markers | | `coach:reports` | Read roster athletes' fueling rollups, AI reports, and the roster digest | | `coach:alerts` | Create and manage the coach's **own** alert rules and report settings | | `coach:webhooks` | Register and manage the coach's **own** concern webhooks | When coach scopes are requested, the consent screen discloses that the app will be able to read the coach's athletes' fueling data and manage the coach's alert settings. Request only the scopes you need. Athletes are more likely to consent when the request is minimal. ## Client registration 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, 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 CIMD is the [MCP 2025-11-25 spec's preferred registration mechanism](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) ([draft-ietf-oauth-client-id-metadata-document](https://datatracker.ietf.org/doc/draft-ietf-oauth-client-id-metadata-document/)). Instead of registering, host a JSON document describing your client at a stable HTTPS URL on a domain you control, and pass that URL as your `client_id`: ```json theme={null} { "client_id": "https://your-app.com/oauth/client-metadata.json", "client_name": "Your App", "redirect_uris": ["https://your-app.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none" } ``` Saturday fetches, validates, and caches the document when it first sees your `client_id`. Requirements: * The URL must be `https`, contain a path, and carry no fragment or userinfo. * The document's `client_id` field must **exactly match** the document URL. * `redirect_uris` must be `https` URLs or loopback (`http://localhost` / `http://127.0.0.1`) URIs. Loopback URIs match on any port (RFC 8252). * 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, 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, with no manual configuration. Saturday serves the standard metadata documents, derived per-environment from the request host: | Document | Path | Spec | | ----------------------------- | ----------------------------------------------- | -------- | | Protected Resource Metadata | `/.well-known/oauth-protected-resource[/mcp]` | RFC 9728 | | Authorization Server Metadata | `/.well-known/oauth-authorization-server[/mcp]` | RFC 8414 | An unauthenticated request to `/mcp` returns `401` with a `WWW-Authenticate: Bearer resource_metadata="…"` header pointing at the discovery document, which bootstraps the OAuth handshake. The connector is a **public client** (PKCE, no client secret) and may use **loopback redirects** (`http://127.0.0.1:`, RFC 8252) for local CLIs such as Claude Code. ## Resource indicators (RFC 8707) 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 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 | | ---------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `resource` | Recommended | Canonical URI of the MCP server (`https://api.saturday.fit/mcp`). The MCP 2025-11-25 spec has clients always send it; older clients may omit it. | A `resource` that is malformed (not an absolute URI, or carries a fragment) or names a resource this server does not serve is rejected with `invalid_target`. On refresh, a `resource` may match the originally-granted resource but cannot retarget the token. ## Error scenarios 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 revoke programmatically: ```bash theme={null} POST /v1/oauth/token/revoke Content-Type: application/x-www-form-urlencoded token=REFRESH_TOKEN ``` Per RFC 7009 this returns `200` whether or not the token existed, so a repeated revoke is not an error. # Athlete Onboarding Source: https://docs.saturday.fit/guides/onboarding Four ways to collect an athlete fueling profile, and why exact numbers require it Saturday never serves exact-looking numbers computed from guesses. Every calculation response tells you exactly how complete the athlete's fueling profile is, what's missing, and how to fix it. This guide covers the precision model and the four ways an athlete's answers can reach Saturday. ## The precision model Calculation responses carry a `precision` object on every tier: ```json theme={null} { "tier": "full", "carb_range_g_per_hr": "60-80", "precision": { "profile_complete": false, "missing_fields": [ { "field": "sweat_level", "required": true, "display_label": "how much you sweat", "band_impact": { "carb_g_per_hr": 0, "sodium_mg_per_hr": 100, "fluid_ml_per_hr": 100 } } ], "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..." } } } ``` * **`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). `missing_fields` arrives sorted most-impactful-first, so it 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, rounded to the same increments the bands use, and says roughly how much narrower the band gets once that field is answered. Take `missing_fields` in the order it arrives rather than re-sorting on these numbers, since rounding can leave two fields tied. ### 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. 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`, and exactness needs all of them. ## Four ways to collect the profile ### 1. Hosted onboarding page (recommended start) 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. * 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) ```bash theme={null} GET /v1/onboarding/questions ``` 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. 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 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`, 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 ```bash theme={null} GET /v1/athletes?profile_complete=false ``` 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. # Organizations Source: https://docs.saturday.fit/guides/organizations Teams, member directories, seat licensing, and negotiated discounts # Organizations 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 theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/organizations", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "display_name": "Seattle Running Club", "description": "Marathon training group", "sport": "run", }, ) org = response.json() # 201 Created org_id = org["id"] ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/organizations", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ display_name: "Seattle Running Club", description: "Marathon training group", sport: "run", }), }); const org = await response.json(); // 201 Created const orgId = org.id; ``` | Field | Required | Description | | -------------- | -------- | ----------------------------------------------- | | `display_name` | Yes | Human-readable organization name | | `description` | No | Free text | | `sport` | No | Primary sport (e.g. `run`, `bike`, `triathlon`) | The response includes server-set `id`, `created_at`, `updated_at`, and a maintained `member_count`. ## Managing members 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 theme={null} import requests import os response = requests.post( f"https://api.saturday.fit/v1/organizations/{org_id}/members", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "email": "coach@seattlerunclub.com", "role": "admin", "athlete_id": "a1b2c3d4-...", # optional link to an athlete record }, ) ``` ```typescript TypeScript theme={null} const response = await fetch( `https://api.saturday.fit/v1/organizations/${orgId}/members`, { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ email: "coach@seattlerunclub.com", role: "admin", athlete_id: "a1b2c3d4-...", // optional link to an athlete record }), } ); ``` ### Member roles | Role | Meaning | | -------- | --------------------------------------------- | | `admin` | Organization manager (coach, team admin) | | `member` | Everyone else. Applied when `role` is omitted | Any other value returns `400`. ### Listing and removing members ```bash theme={null} GET /v1/organizations/{org_id}/members DELETE /v1/organizations/{org_id}/members/{member_id} ``` Removing a member never deletes an athlete profile; it removes the directory entry and decrements `member_count`. ## Seat subscriptions (team licensing) A seat subscription is a block of **paid seats** the organization holds; assigning a license to an athlete grants them full-precision API responses while the subscription is active and in its date window. This is the lane for orgs that pay for their athletes' access directly (vs. athletes paying at personal checkout). ### Create a seat block ```bash theme={null} POST /v1/organizations/{org_id}/subscriptions ``` ```json theme={null} { "type": "team", "seats": 25, "start_date": 1765467600, "end_date": 1797003600 } ``` `type` is `"team"` or `"enterprise"`; dates are unix seconds (`end_date` optional). The response includes `used_seats` (starts at 0) and `status`. Manage with `GET /v1/organizations/{org_id}/subscriptions` and `PATCH .../subscriptions/{sub_id}` (update `seats`, `status`, `end_date`). ### Assign licenses to athletes ```bash theme={null} POST /v1/organizations/{org_id}/subscriptions/{sub_id}/licenses ``` ```json theme={null} { "licenses": [ { "athlete_id": "a1b2c3d4-...", "email": "rider@example.com", "name": "Sam Rider" } ] } ``` `athlete_id` is required per license (email/name are display metadata); the seat limit is enforced. The licensed athlete's next API call returns full precision. `GET .../licenses` lists assignments; `DELETE .../licenses/{athlete_id}` frees the seat (access degrades on the athlete's next call). ## Organization offers (negotiated discounts) 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 rather than deleted, so the document trail is the negotiation audit log. ### Setting the offer ```bash theme={null} PUT /v1/organizations/{org_id}/offer ``` ```python Python theme={null} import requests import os response = requests.put( f"https://api.saturday.fit/v1/organizations/{org_id}/offer", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "discount_percent": 15, "applies_to_plan": "12_month", "negotiated_by": "Coach Dana Reyes", "note": "2026 season team agreement", }, ) ``` ```typescript TypeScript theme={null} const response = await fetch( `https://api.saturday.fit/v1/organizations/${orgId}/offer`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ discount_percent: 15, applies_to_plan: "12_month", negotiated_by: "Coach Dana Reyes", note: "2026 season team agreement", }), } ); ``` | Field | Required | Description | | ----------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `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, 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 | The response echoes the stored offer plus `stack_cap_percent`, the hard ceiling on the total stacked discount (currently `30`). ### Reading and removing the offer ```bash theme={null} GET /v1/organizations/{org_id}/offer # current active offer; 404 when none DELETE /v1/organizations/{org_id}/offer # deactivates it; idempotent, returns 204 ``` ### Which athletes get it The offer applies to athletes whose `org_id` field references this organization. Assert it when creating or updating the athlete: ```bash theme={null} PATCH /v1/athletes/{athlete_id} ``` ```json theme={null} { "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: 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%**. 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. ## Listing organizations ```bash theme={null} GET /v1/organizations ``` Returns all organizations created by your partner account. ## Updating an organization ```bash theme={null} PATCH /v1/organizations/{org_id} ``` ```json theme={null} { "display_name": "Seattle Running Club - Elite Squad", "description": "Elite marathon squad" } ``` ## Finding an organization's athletes Athletes reference their organization through the `org_id` field on the athlete record (`POST /v1/athletes` and `PATCH /v1/athletes/{athlete_id}`, 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. # Safety Source: https://docs.saturday.fit/guides/safety Saturday's safety model: why it exists and how it works # Safety 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. 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 | Condition | Cause | Consequence | | ------------------ | ----------------------------------------- | -------------------------------- | | **Hyponatremia** | Overdrinking, insufficient sodium | Confusion, seizures, coma, death | | **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 | 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, teaser and full alike, includes a `safety` object: ```json theme={null} { "safety": { "max_safe_fluid_ml_per_hr": 1500, "max_safe_sodium_mg_per_hr": 3000, "confidence_score": 0.72, "requires_human_review": false, "warnings": [ "Fluid intake exceeds recommended maximum. Consider reducing duration or consulting a sports dietitian." ], "not_instructions": true } } ``` Safety data is never gated behind subscription status. A teaser response shows ranges instead of exact numbers, and carries the same safety block. ### The two threshold fields `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. ### Warnings `warnings` is populated by comparing the prescription against those two thresholds. Two warnings exist: | 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." | 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. ### Where each safety field is populated The safety block is present on every prescription response, but not every endpoint fills all of it. | 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` | 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. ## Engine limits Saturday bounds every calculation. The limits are computed per athlete and per activity, then clamped by absolutes that no input combination can exceed. | Guardrail | How it is bounded | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Fluid | A fixed ceiling no prescription crosses. Thermal stress and intensity tighten the usable range well below it | | Sodium | A ceiling that scales with body weight, under a fixed absolute maximum. Carb experience, intensity, and thermal stress narrow the range | | Sodium floor | Rises with thermal stress. Hot conditions cannot produce a low-sodium prescription | | Carbohydrate | A ceiling narrowed by `carb_upper_limit_override`, by stated carb experience, and by whether the athlete is prioritizing performance. An athlete who has never fueled at high carbohydrate rates is not handed an aggressive target | | Duration scaling | Intake ranges are a function of duration, so a 6-hour target is not a 1-hour rate multiplied out | These apply on every calculation and are not partner-overridable. That is deliberate. A caller able to raise a ceiling could raise it past what is safe for that athlete, and the athlete would have no way to know it happened. The ceilings are not uniform either: several scale with the athlete, so a smaller athlete is held to a tighter limit than a larger one on the same session. Teaser ranges are display buckets, and both ends are bounded. The upper bound is clamped to the engine's ceiling, so a bucket never implies an intake the engine would not prescribe. 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. 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. ### When a prescription reads lower than expected A number that reads low usually has one of these causes: * **The athlete capped themselves.** `carb_upper_limit_override` is a ceiling the athlete sets on their own carbohydrate, and the engine does not exceed it. * **Stated carb experience is low.** An athlete who reports never having fueled at high carbohydrate rates is held below a target they have not trained for. * **`performance` is `false`.** An athlete who is not prioritizing performance gets a lower carbohydrate ceiling. * **Conditions are cool.** Low thermal stress pulls fluid and sodium down sharply. Overdrinking without sodium is the hyponatremia mechanism, so the engine is conservative in the cold by design. * **The session is long.** Hourly rates are a function of duration rather than a constant, so a long session's per-hour target is not the short-session rate carried forward. * **The profile is incomplete.** Check `precision.missing_fields`. A defaulted field widens the band and moves the number. Sending the same activity with a fuller profile is the fastest way to tell a guardrail from a gap in the data. ## Confidence score `confidence_score` (0.0-1.0) reports how much of the athlete's fueling profile was answered. 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). | 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 | `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`. ## The `not_instructions` field Every prescription response carries `not_instructions: true`. 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. ## Display requirements for partners ### Required * 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. ### Recommended * 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 * 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 Saturday's AI coach carries eating-disorder handling: it keeps to performance framing and avoids calorie, weight, and restriction language. 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. 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. # Webhooks Source: https://docs.saturday.fit/guides/webhooks Event notifications signed with HMAC-SHA256 # Webhooks 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 theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}"}, json={ "url": "https://your-app.com/webhooks/saturday", "events": [ "prescription.calculated", "athlete.updated", "athlete.created", ], }, ) webhook = response.json() # Save webhook["secret"]: you need it to verify signatures print(f"Webhook secret: {webhook['secret']}") ``` ```typescript TypeScript theme={null} const response = await fetch("https://api.saturday.fit/v1/webhooks", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ url: "https://your-app.com/webhooks/saturday", events: ["prescription.calculated", "athlete.updated", "athlete.created"], }), }); const webhook = await response.json(); // Save webhook.secret: you need it to verify signatures console.log(`Webhook secret: ${webhook.secret}`); ``` **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.cancelled` | An athlete's access degrades to teaser (cancellation, expiry, or refund) | 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 and never before. A resubscribe after cancellation fires it again. ```json theme={null} { "id": "6f1c9a02-4d3b-4c1e-9f77-2b8a5d0e1c34", "type": "subscription.created", "created_at": 1765467600000, "data": { "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "tier": "full", "expires_at": 1797003600000, "source": "checkout_subscription" } } ``` `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 theme={null} { "id": "8a2d7e14-9b60-4f33-a1c5-7d4e6b90f218", "type": "subscription.cancelled", "created_at": 1765467600000, "data": { "athlete_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d", "tier": "teaser", "reason": "customer.subscription.deleted" } } ``` 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. `id` is a UUID and `created_at` is a Unix timestamp in **milliseconds**: ```json theme={null} { "id": "3e9b1f27-5c84-4a19-b2d6-8f70c3a51e4b", "type": "prescription.calculated", "created_at": 1765467600000, "data": { "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 } } ``` 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 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` | 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 1. Extract the timestamp and signature from the header 2. Reconstruct the signed payload: `{timestamp}.{raw_body}` 3. Compute HMAC-SHA256 using your webhook secret 4. Compare with constant-time equality ```python Python theme={null} import hashlib import hmac import time from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = "whsec_your_webhook_secret_here" @app.route("/webhooks/saturday", methods=["POST"]) def handle_webhook(): # 1. Extract signature header signature_header = request.headers.get("X-Saturday-Signature") if not signature_header: abort(400, "Missing signature header") # 2. Parse timestamp and signature parts = signature_header.split(",") timestamp = None signature = None for part in parts: key, value = part.split("=", 1) if key == "t": timestamp = value elif key == "v1": signature = value if not timestamp or not signature: abort(400, "Invalid signature format") # 3. Reject stale timestamps (5-minute replay window) if abs(time.time() - int(timestamp)) > 300: abort(400, "Timestamp outside the replay window") # 4. Compute expected signature raw_body = request.get_data(as_text=True) signed_payload = f"{timestamp}.{raw_body}" expected = hmac.new( WEBHOOK_SECRET.encode("utf-8"), signed_payload.encode("utf-8"), hashlib.sha256, ).hexdigest() # 5. Constant-time comparison if not hmac.compare_digest(expected, signature): abort(400, "Invalid signature") # 6. Process the event event = request.json print(f"Received event: {event['type']}") # Return 200 quickly; process async if needed return "", 200 ``` ```typescript TypeScript theme={null} import crypto from "crypto"; import express from "express"; const app = express(); const WEBHOOK_SECRET = "whsec_your_webhook_secret_here"; app.post( "/webhooks/saturday", express.raw({ type: "application/json" }), (req, res) => { // 1. Extract signature header const signatureHeader = req.headers["x-saturday-signature"] as string; if (!signatureHeader) { return res.status(400).send("Missing signature header"); } // 2. Parse timestamp and signature const parts = Object.fromEntries( signatureHeader.split(",").map((p) => p.split("=", 2) as [string, string]) ); const timestamp = parts["t"]; const signature = parts["v1"]; if (!timestamp || !signature) { return res.status(400).send("Invalid signature format"); } // 3. Reject stale timestamps (5-minute replay window) if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) { return res.status(400).send("Timestamp too old"); } // 4. Compute expected signature const rawBody = req.body.toString("utf-8"); const signedPayload = `${timestamp}.${rawBody}`; const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(signedPayload) .digest("hex"); // 5. Constant-time comparison if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) { return res.status(400).send("Invalid signature"); } // 6. Process the event const event = JSON.parse(rawBody); console.log(`Received event: ${event.type}`); // Always return 200 quickly res.status(200).send(); } ); ``` ## Retry behavior 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 | 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. Redirects are not followed: a `3xx` counts as a failure, so register the final URL rather than a redirector. ## Auto-disable 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 theme={null} curl -X PATCH https://api.saturday.fit/v1/webhooks/{webhook_id} \ -H "Authorization: Bearer $SATURDAY_API_KEY" \ -d '{"active": true}' ``` ## Best practices 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 ```bash theme={null} 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 ``` `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`. Subscribing to it is valid but unnecessary: the test delivery reaches your endpoint regardless. Your handler should still ignore event types it does not recognize rather than erroring on them. `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. # Introduction Source: https://docs.saturday.fit/introduction Fuel, hydration, and electrolyte prescriptions for endurance athletes, as an API # Saturday API 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 the API returns Training platforms build the workout. Saturday fuels it. | 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 | Safety metadata is present on every response at every tier, including free ones. See [Safety](/guides/safety). 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. ## The problem 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. Saturday's API lets a platform offer that without building it. Your platform handles the training. Saturday handles the fueling. ## Who builds on it * 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 ## What it costs 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. 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 guides | Guide | Covers | | ------------------------------------------------------ | --------------------------------------------------- | | [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 Your first prescription, start to finish API keys, environments, and security What the safety metadata means The prescription endpoint in depth # Quickstart Source: https://docs.saturday.fit/quickstart Your first prescription, start to finish # Quickstart ## 1. Get your API key **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). 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. The examples below read the key from a `SATURDAY_API_KEY` environment variable, so the same code runs against either environment: ```bash theme={null} export SATURDAY_API_KEY="sk_live_..." ``` ## 2. Make your first calculation 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 theme={null} curl -X POST https://api.saturday.fit/v1/nutrition/calculate \ -H "Authorization: Bearer $SATURDAY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "activity_type": "run", "duration_min": 90, "intensity_level": 5, "athlete_weight_kg": 70, "thermal_stress_level": 6 }' ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.saturday.fit/v1/nutrition/calculate", headers={ "Authorization": f"Bearer {os.environ['SATURDAY_API_KEY']}", "Content-Type": "application/json", }, json={ "activity_type": "run", "duration_min": 90, "intensity_level": 5, "athlete_weight_kg": 70, "thermal_stress_level": 6, }, ) data = response.json() print(data) ``` ```typescript TypeScript theme={null} const response = await fetch( "https://api.saturday.fit/v1/nutrition/calculate", { method: "POST", headers: { Authorization: `Bearer ${process.env.SATURDAY_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ activity_type: "run", duration_min: 90, intensity_level: 5, athlete_weight_kg: 70, thermal_stress_level: 6, }), } ); const data = await response.json(); console.log(data); ``` ### Install an SDK (optional) Saturday publishes SDKs for Python and TypeScript: ```bash Python theme={null} pip install saturday ``` ```bash TypeScript theme={null} npm install @saturdayinc/sdk ``` ```python Python (SDK) theme={null} import os from saturday import Saturday client = Saturday(api_key=os.environ["SATURDAY_API_KEY"]) result = client.nutrition.calculate( activity_type="run", duration_min=90, intensity_level=5, athlete_weight_kg=70, thermal_stress_level=6, ) print(result) ``` ```typescript TypeScript (SDK) theme={null} import Saturday from "@saturdayinc/sdk"; const client = new Saturday({ apiKey: process.env.SATURDAY_API_KEY }); const result = await client.nutrition.calculate({ activity_type: "run", duration_min: 90, intensity_level: 5, athlete_weight_kg: 70, thermal_stress_level: 6, }); 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 prescription with safety metadata: ```json theme={null} { "tier": "full", "carb_g_per_hr": 60.0, "sodium_mg_per_hr": 600.0, "fluid_ml_per_hr": 620.0, "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, "confidence_score": 0.72, "requires_human_review": false, "warnings": [], "not_instructions": true }, "attribution": { "text": "Powered by Saturday", "logo_url": "https://saturday.fit/logo.png", "link": "https://saturday.fit", "required": false } } ``` | Field | What it means | | ------------------------------ | ------------------------------------------------- | | `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_*` | 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 carries full safety metadata regardless of subscription status, because overdrinking can cause hyponatremia, a potentially fatal condition. ## 4. Teaser and full responses `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 or in trial** | Exact numbers | `"carb_g_per_hr": 60.0` | | **Free** | Ranges | `"carb_range_g_per_hr": "60-90"` | Teaser responses carry a `subscription_cta` field, and their `attribution.required` is `true`: ```json theme={null} { "tier": "teaser", "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": "Get your exact carb, sodium, and fluid targets, not ranges", "subscribe_url": "https://saturday.fit/subscribe?ref=YOUR_PARTNER_ID", "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 how the tiers are decided and how the subscribe loop pays you. ## 5. Next steps [Athletes](/guides/athletes) stores athlete settings (weight, sweat level, preferences) so calculations stop relying on defaults. [Safety](/guides/safety) covers the safety model and the ceilings above. [Activities](/guides/activities) creates activities, attaches prescriptions, and collects post-session feedback. [Authentication](/authentication) covers swapping a `sk_test_` key and its sandbox base URL for a `sk_live_` key against `api.saturday.fit`. # Rate Limiting Source: https://docs.saturday.fit/rate-limiting Rate limit headers, ceilings, and handling 429s # Rate Limiting 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 authenticated response carries the current bucket state: ```http theme={null} HTTP/1.1 200 OK X-RateLimit-Limit: 120 X-RateLimit-Remaining: 4 X-RateLimit-Reset: 0 Content-Type: application/json ``` | Header | Description | | ----------------------- | ------------------------------------------------------------------ | | `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 Exceeding a ceiling returns `429 Too Many Requests`: ```json theme={null} { "error": { "type": "rate_limit_error", "code": "rate_limit_exceeded", "message": "Rate limit exceeded. Retry after 1 seconds.", "documentation_url": "https://docs.saturday.fit/errors#rate_limit_exceeded", "request_id": "req_abc123def456" } } ``` 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: ```python Python theme={null} import time response = make_api_request() if response.status_code == 429: retry_after = int(response.headers.get("Retry-After", 60)) time.sleep(retry_after) response = make_api_request() ``` ```typescript TypeScript theme={null} const response = await makeApiRequest(); if (response.status === 429) { const retryAfter = parseInt(response.headers.get("Retry-After") ?? "60"); await new Promise((r) => setTimeout(r, retryAfter * 1000)); return makeApiRequest(); } ``` 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). ## Daily call limits Alongside the token bucket, every account carries daily call ceilings: | Account | Daily ceiling | | -------------------------- | -------------------------------------------- | | Self-serve | 200 calls/day | | Partner, no agreed ceiling | Platform default (currently 2,000 calls/day) | | Contracted integration | Per agreement, up to no ceiling at all | Intelligence endpoints (nutrition, products, inference, knowledge, AI) also carry their own daily ceiling, separate from the total. Data endpoints (your athletes, activities, organizations, webhooks) are not counted against it, so hitting the intelligence ceiling never blocks syncing your own data. Without an agreed intelligence ceiling, your budget grows with your day: a base allowance plus a per-athlete allowance for every athlete you compute for that day. Integrations serving real athletes scale automatically; if your traffic pattern needs more than the budget provides, contact [api@saturday.fit](mailto:api@saturday.fit). Request-pattern friction (`rate_limited`) also bounds daily variety on a few surfaces: distinct product lookups, distinct search queries, and calculations that carry no athlete. Repeats always serve, so a real workload (your athletes' usual products, revisited) never feels these; enumeration does. These 429s carry no `Retry-After`; novel inputs resume at midnight UTC. Reaching a ceiling returns `429 Too Many Requests` without a `Retry-After` header; the message names the limit, and counters reset at midnight UTC. `GET /v1/partner` reports the ceilings that apply to your account (`rate_limits.requests_per_day` and `rate_limits.ip_requests_per_day`, `null` when none applies). To raise yours, contact [api@saturday.fit](mailto:api@saturday.fit). ## Staying under the ceilings ### Spread requests out 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. ### Cache prescriptions A prescription for the same inputs doesn't change on its own. Cache the result and recalculate when: * 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 ### Use the batch endpoint ```bash theme={null} POST /v1/nutrition/calculate/batch ``` 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). ### Watch your usage ```bash theme={null} curl https://api.saturday.fit/v1/partner/usage \ -H "Authorization: Bearer $SATURDAY_API_KEY" ``` `GET /v1/partner/usage/daily` breaks the same counters down by day. ## Requesting higher limits 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.