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

# 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-80"`      | Exact: `"carb_g_per_hr": 62.5`   |
| **Hydration**        | Range: `"fluid_range_ml_per_hr": "600-900"`  | Exact: `"fluid_ml_per_hr": 620`  |
| **Sodium**           | Range: `"sodium_range_mg_per_hr": "300-600"` | Exact: `"sodium_mg_per_hr": 485` |
| **Products**         | Category only ("gel")                        | Specific products + schedule     |
| **Safety metadata**  | Full                                         | Full                             |
| **Confidence score** | Shown                                        | Shown                            |

<Note>
  **Safety is never gated.** Both teaser and full responses include complete safety metadata. Safety information is always free.
</Note>

## Detecting response type

<CodeGroup>
  ```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);
  }
  ```
</CodeGroup>

## 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`/`PATCH /v1/athletes`). Matching runs when you write an athlete email, when a Saturday subscription activates, and in a nightly sweep.
* **You get the same webhook.** When a match links an actively paying account, `subscription.created` fires with `source: "email_match"` — handle it exactly like a checkout unlock.
* **Ambiguity never auto-links.** Multiple athletes sharing an email, or a conflict with an existing link, goes to Saturday-side human review instead.
* **No revenue share on matched links.** These subscriptions weren't driven through your platform (many predate it), so they don't appear on your revenue statement.
* **Only a tier boolean is released.** The match tells your platform the athlete's subscription tier — never payment details, purchase history, or Saturday profile data.

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.

<Note>
  **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.
</Note>

## Attribution requirements

### Required (teaser tier)

* **Visual attribution**: "Powered by Saturday" or the Saturday logo near any teaser nutrition data
* **Link**: Attribution must link to `saturday.fit` or the `subscribe_url`

### Example implementation

```html theme={null}
<div class="nutrition-teaser">
  <p>Carbohydrates: 45-75 g/hr</p>
  <p>Hydration: 450-750 mL/hr</p>
  <p>Sodium: 350-650 mg/hr</p>

  <a href="https://saturday.fit/subscribe?ref=your_partner_id">
    Get exact targets — Powered by Saturday
  </a>
</div>
```

### Not required (full tier)

Attribution is appreciated but not required when displaying data for subscribed athletes.

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

<Note>
  **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.
</Note>
