---
title: "Post-workout paywall for fitness apps | Snowplow Signals"
description: "Snowplow Signals tells your paywall when a workout ends and what this user has done in the trial, so it shows the offer then, with the plan that fits."
source: https://signals.snowplow.io/solutions/health-fitness/paywall
---

[Health and fitness](https://signals.snowplow.io/solutions/health-fitness) / Post-workout paywall 

# A paywall that waits for the workout to end. 

Signals tells your paywall the moment a workout ends and what this user has done in the trial, so the offer shows when there is something to keep.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Higher trial-to-subscription conversion

## Convert more trials by making the offer after a workout, not on first open. 

Trial conversion moves when the offer lands after the user has done something they would pay to keep. Signals gives your paywall the minutes since the last workout ended, how many workouts this trial has had and which premium feature this user keeps coming back to, so it chooses when to show the offer, which plan to lead with and what to name in it.

Four ways fitness apps use it

01 Trial users who just finished a workout First workout done, on the summary screen. Show the offer on the next one. 02 Trial users on day one Installed an hour ago, no workout yet. Hold the offer. 03 Trial users who keep using one premium feature Three guided runs this week. Name them in the offer. 04 Trial users in the last days who trained most days Two days left, a workout on most of them. Offer the year, not the month. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Show the offer on the screen after the workout ends

A trial user has just finished their first workout and is reading the summary. The offer was shown when they first opened the app, before they had done anything in it, and it has not come back since. The moment they have something worth keeping is this one, and the offer belongs on the next screen they see.

What Signals knows

`minutes_since_workout` 

Minutes since their last workout ended

`workouts_trial` 

Workouts completed since their first open

What your paywall does

Shows the offer on the screen after the summary when a workout ended in the last fifteen minutes, and holds it on every other open so the first screen of the day is not a price.

Define it · PythonUse it · TypeScript

summary.tsx · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, StreamAttributeGroup, user_id

workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

minutes_since_workout = Attribute(
    name="minutes_since_workout",
    type="double",
    events=[workout_completed],
    aggregation="time_since_last",
    time_unit="min",
)

workouts_trial = Attribute(
    name="workouts_trial",
    type="int32",
    events=[workout_completed],
    aggregation="counter",
    ttl=timedelta(days=30),
    default_value=0,
)

# Trial users are signed in, so the group keys on the user id.
fit_paywall_finished = StreamAttributeGroup(
    name="fit_paywall_finished",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[minutes_since_workout, workouts_trial],
)
```

```
// In the code that renders the screen after the workout summary
const user = await signals.getServiceAttributes({
  name: "fit_paywall_finished",
  attribute_key: "user_id",
  identifier: userId,
});
// { minutes_since_workout: 2.4, workouts_trial: 1 }

if (user.minutes_since_workout <= 15 && user.workouts_trial >= 1) {
  paywall.show({
    plan: "monthly",
    headline: "Keep this one in your history",
  });
}
```

### Hold the offer until the user has something to keep

A user installed the app an hour ago, picked a goal and has not started a workout. The offer tends to appear on this first open, so it asks for a payment before there is a streak or a session to keep. Day one with nothing done is the moment to open on the first session and let the offer wait.

What Signals knows

`days_since_first_open` 

Days since they first opened the app

`workouts_trial` 

Workouts completed since their first open

What your paywall does

Keeps the offer off the screen while the user is on day one with no workout completed, and opens on the first session of their plan instead. The offer comes back once a workout ends.

Define it · PythonUse it · TypeScript

home.tsx · 12 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, StreamAttributeGroup, user_id

app_opened = Event(name="app_opened", vendor="com.example", version="1-0-0")
workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

# The first open starts the trial, so time since it is the day of the trial.
days_since_first_open = Attribute(
    name="days_since_first_open",
    type="double",
    events=[app_opened],
    aggregation="time_since_first",
    time_unit="d",
)

workouts_trial = Attribute(
    name="workouts_trial",
    type="int32",
    events=[workout_completed],
    aggregation="counter",
    ttl=timedelta(days=30),
    default_value=0,
)

fit_paywall_day_one = StreamAttributeGroup(
    name="fit_paywall_day_one",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[days_since_first_open, workouts_trial],
)
```

```
// In the code that renders the home screen
const user = await signals.getServiceAttributes({
  name: "fit_paywall_day_one",
  attribute_key: "user_id",
  identifier: userId,
});
// { days_since_first_open: 0.04, workouts_trial: 0 }

if (user.days_since_first_open < 1 && user.workouts_trial === 0) {
  paywall.hold();
  home.open({ screen: "first_session" });
}
```

### Name the feature they keep using in the offer

A trial user has opened the guided runs three times this week and little else from the premium set. The offer lists every premium feature at once, so the one thing they keep using is one line among many. The headline should name the guided runs, and the plan under it should be the one that includes them.

What Signals knows

`top_premium_feature_trial` 

The premium feature they have used most since their first open

`premium_uses_trial` 

How many times they used each premium feature since their first open

What your paywall does

Puts the feature the user keeps using in the headline of the offer once they have used it three times, and leads with the plan that includes it.

Define it · PythonUse it · TypeScript

home.tsx · 17 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

premium_feature_used = Event(name="premium_feature_used", vendor="com.example", version="1-0-0")
# The feature is the name of what the app opened, never a reading taken from the user.
feature = EventProperty(vendor="com.example", name="premium_feature_used", major_version=1, path="feature")

top_premium_feature_trial = Attribute(
    name="top_premium_feature_trial",
    type="string",
    events=[premium_feature_used],
    aggregation="most_frequent",
    property=feature,
    ttl=timedelta(days=30),
)

premium_uses_trial = Attribute(
    name="premium_uses_trial",
    type="dict",
    events=[premium_feature_used],
    aggregation="category_count",
    property=feature,
    ttl=timedelta(days=30),
)

fit_paywall_feature = StreamAttributeGroup(
    name="fit_paywall_feature",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[top_premium_feature_trial, premium_uses_trial],
)
```

```
// In the code that renders the home screen
const user = await signals.getServiceAttributes({
  name: "fit_paywall_feature",
  attribute_key: "user_id",
  identifier: userId,
});
// { top_premium_feature_trial: "guided_runs", premium_uses_trial: { guided_runs: 3, video_classes: 1 } }

const top = user.top_premium_feature_trial;
const uses = user.premium_uses_trial[top] ?? 0;

if (uses >= 3) {
  paywall.show({
    plan: plans.including(top),
    headline: `Keep the ${plans.label(top)}`,
  });
}
```

### Lead with the annual plan when the habit is forming

A trial user has two days left and has completed a workout on most of the days so far. The offer leads with the monthly plan for everyone, and the annual plan sits behind a toggle. This user is forming a habit, and the plan that matches it is the year. The trial end date comes from your billing system and the training record comes from Signals.

What Signals knows

`days_since_first_open` 

Days since they first opened the app

`workouts_trial` 

Workouts completed since their first open

`days_since_workout` 

Days since their last workout ended

What your paywall does

Leads with the annual plan when a user in the last days of the trial has completed a workout on most of them and trained in the last day or two, and keeps the monthly plan first for everyone else.

Define it · PythonUse it · TypeScript

home.tsx · 18 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, StreamAttributeGroup, user_id

app_opened = Event(name="app_opened", vendor="com.example", version="1-0-0")
workout_completed = Event(name="workout_completed", vendor="com.example", version="1-0-0")

days_since_first_open = Attribute(
    name="days_since_first_open",
    type="double",
    events=[app_opened],
    aggregation="time_since_first",
    time_unit="d",
)

workouts_trial = Attribute(
    name="workouts_trial",
    type="int32",
    events=[workout_completed],
    aggregation="counter",
    ttl=timedelta(days=30),
    default_value=0,
)

days_since_workout = Attribute(
    name="days_since_workout",
    type="double",
    events=[workout_completed],
    aggregation="time_since_last",
    time_unit="d",
)

fit_paywall_last_days = StreamAttributeGroup(
    name="fit_paywall_last_days",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[days_since_first_open, workouts_trial, days_since_workout],
)
```

```
// In the code that renders the home screen
const user = await signals.getServiceAttributes({
  name: "fit_paywall_last_days",
  attribute_key: "user_id",
  identifier: userId,
});
// { days_since_first_open: 12.4, workouts_trial: 9, days_since_workout: 0.8 }

const trainedMostDays = user.workouts_trial / user.days_since_first_open >= 0.6;

if (trial.daysLeft <= 2 && trainedMostDays && user.days_since_workout <= 2) {
  paywall.show({
    plan: "annual",
    headline: "Keep the habit going for a year",
  });
} else if (trial.daysLeft <= 2) {
  paywall.show({ plan: "monthly" });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
