> For the complete documentation index, see [llms.txt](https://docs.tryterra.co/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tryterra.co/unified-api/strength-training.md).

# Strength training data

Strength workouts carry per-set detail on the **activity** payload under `strength_data` — no separate endpoint, no new webhook type. If you already receive activity data, you already receive this.

**This is additive.** No existing field changes. `strength_data` sits alongside `heart_rate_data` and the rest, and is `null` on any activity with no strength content — which is most of them. The only change most integrations need is tolerating one more key.

New to Terra? Start with the [Quickstart](/unified-api/quickstart.md) to receive activity data first.

## Getting access

Nothing to switch on. Strength data is part of the Unified API and available on every account — if you receive activity data from a source that reports sets, `strength_data` is already populated.

Nothing is re-sent, though: **webhooks already delivered are not replayed**. To pick up `strength_data` on workouts you received earlier, [re-request the activity](/unified-api/managing-user-health-data/requesting-historical-data.md) over the REST API, which returns the same payload with `strength_data` filled in.

## Which sources report strength data

Two kinds of source report sets: wearables that detect them, and training apps the user logs into.

| Source        | What the user connects with                                 | Notes                                                                                                                                                                                                                                                                                                                                                                              |
| ------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Garmin        | The usual Garmin connection                                 | Detected on the watch, so `source` is `device_detected` — see [How to interpret the data](#how-to-interpret-the-data). Sets come can be a moment behind the rest of the activity, so expect a later update filling in strength carrying the same `summary_id`.                                                                                                                     |
| Coros         | The usual Coros connection                                  | Detected on the watch, so `source` is `device_detected`.                                                                                                                                                                                                                                                                                                                           |
| TrainingPeaks | The usual TrainingPeaks connection                          | Structured strength sessions, including coach-prescribed ones. Only sets the athlete actually performed are reported — a prescription they skipped is not. Coaches can also write their own exercises: those keep `exercise_name`, `reps` and `weight_kg`, but `movements`, the muscle lists and every `modifiers` axis are `null`, because nothing in the source classifies them. |
| Hevy          | Hevy API key                                                | Logged workouts, identified from Hevy's own catalogue. Also carries body metrics.                                                                                                                                                                                                                                                                                                  |
| wger          | wger username and password                                  | Logged workouts, identified from wger's own catalogue. Also carries body and nutrition.                                                                                                                                                                                                                                                                                            |
| Fitbod        | Fitbod username and password                                | Strength only.                                                                                                                                                                                                                                                                                                                                                                     |
| Strong        | Strong username and password                                | Strength only.                                                                                                                                                                                                                                                                                                                                                                     |
| StrongLifts   | Email address, then a one-time code StrongLifts emails them | Strength only.                                                                                                                                                                                                                                                                                                                                                                     |
| Lyfta         | Lyfta API key                                               | Strength only.                                                                                                                                                                                                                                                                                                                                                                     |
| Liftosaur     | Liftosaur API key                                           | Strength only. The key is a premium-account feature, so free Liftosaur accounts cannot be connected.                                                                                                                                                                                                                                                                               |

More sources are being added. Every one reports through the same `strength_data` shape, so integrating once covers the rest.

## Reading a payload

`strength_data` sits on each entry of the activity payload's `data` array:

```json
{
  "type": "activity",
  "data": [
    {
      "metadata": { "summary_id": "…", "start_time": "…" },
      "strength_data": {
        "body_weight_kg": 78.5,
        "exercises": [
          {
            "exercise_name": "Paused Back Squat",
            "movements": ["squat"],
            "modifiers": { "implement": "barbell", "tempo": "paused" },
            "primary_muscles": ["quadriceps"],
            "secondary_muscles": ["glutes", "hamstrings", "abs"],
            "sets": [
              { "set_type": "warmup", "reps": 10, "weight_kg": 60,  "load_type": "external" },
              { "set_type": "normal", "reps": 4,  "weight_kg": 136, "load_type": "external", "rpe": 8.5 }
            ]
          }
        ]
      }
    }
  ]
}
```

{% hint style="info" %}
Fields are trimmed above for readability. **Terra always emits every documented key** — anything the source didn't report arrives as `null`, not missing. So test `strength_data` for non-null rather than checking whether the key exists, and expect `null` on fields the example omits.
{% endhint %}

**Don't gate on activity type.** Check `strength_data` for non-null instead — strength work can arrive on more than one workout type.

### Over the REST API

The same payload comes back from the activity endpoint, so a backfill and a webhook give you identical objects:

```bash
curl --request GET \
  --url 'https://api.tryterra.co/v2/activity?user_id=<user_id>&start_date=2026-08-01&end_date=2026-08-08' \
  --header 'dev-id: <your-dev-id>' \
  --header 'x-api-key: <your-api-key>'
```

`end_date` is exclusive. See [Requesting historical data](/unified-api/managing-user-health-data/requesting-historical-data.md) for the full endpoint.

## Reading load correctly

`weight_kg` is meaningless on its own. Always read it with `load_type`:

```python
def set_volume_kg(s):
    # Only external load counts. Test FOR external: an unrecognised
    # load type must be treated as unknown, not counted.
    if s["load_type"] != "external":
        return 0
    return (s["weight_kg"] or 0) * (s["reps"] or 0)
```

A pull-up logged at `20 kg` with `load_type: "assisted"` means 20 kg was *removed* to help the lifter. Summing weight across every set counts that as 20 kg added, and does the same for bodyweight sets.

`bodyweight_plus` is excluded above on purpose: its `weight_kg` is the extra load only (the belt on a weighted pull-up), so total load needs `strength_data.body_weight_kg` added in. Include it if you want total load rather than added load.

Two more things `weight_kg` will not tell you:

* It is the load **as the source logged it** — a barbell's total, but a single dumbbell for dumbbell work. Sources do not agree on whether to double it, and Terra does not normalise it, because guessing wrong corrupts a lifter's history in either direction.
* `reps` is **per side** for unilateral movements, so check `modifiers.laterality` before doubling anything.

## Exercises and sets

An exercise is one or more `movements` plus a set of `modifiers`, not a single name from a fixed list — so **matching on `movements` gives you every variant of a lift**, including ones added after you wrote the query. A paused squat is `movements: ["squat"]` with `modifiers.tempo: "paused"`.

`movements` is a list because one exercise can chain several: a clean and jerk is `["clean", "jerk"]` and belongs in both clean volume and jerk volume. **Test membership, never equality:**

```python
def squat_volume_kg(strength_data):
    total = 0
    for exercise in strength_data["exercises"] or []:
        if "squat" not in (exercise["movements"] or []):
            continue          # catches front, box, paused, Zercher, sumo, …
        for s in exercise["sets"]:
            total += set_volume_kg(s)
    return total
```

Read `modifiers` only when the variant matters — `modifiers.implement == "barbell"` to exclude goblet squats, say. An axis that is `null` means the source **didn't report it**, not that the default applies: a squat with no `implement` is not a bodyweight squat.

Sets are nested inside their exercise in the order performed. Exercises performed together in a superset or circuit share a `group_id`.

An exercise can arrive with an empty `sets` array — the user added it to the workout and logged nothing against it. Filter those out if you only want performed work.

### When an exercise doesn't resolve

`movements` can be `null`. It happens most often with exercises the user or their coach wrote themselves, which the source stores as free text and never classifies:

```json
{ "exercise_name": "Leg Press mit beiden Beinen", "movements": null,
  "modifiers": null,
  "sets": [ { "reps": 12, "weight_kg": 120, "load_type": "external" } ] }
```

The sets are still complete, so volume and load work as normal — only the rollup is missing. `exercise_name` always carries the source's own label, so display keeps working.

Don't parse `exercise_name`. Its shape follows the source: a training app sends what the user sees (`Close Grip Bench Press`), a watch sends its own catalogue key (`triceps_pressdown`). Use it for display, and `movements` and `modifiers` for logic.

## How to interpret the data

### `source` tells you where a number came from

The same field means different things depending on how the exercise was captured, and it's worth surfacing that difference to your users rather than presenting both identically:

* `user_logged` — the user recorded it in a training app. Reps and load are exactly what they entered.
* `device_detected` — a wearable inferred it from motion. Reps and the exercise itself are estimates, and `weight_kg` is `null` more often: the device doesn't measure load, so it's present only when the user added it afterwards.

A watch also won't always resolve a set into an exercise. Where it recorded that work happened but couldn't classify it, the sets are reported with `movements` unresolved rather than guessed — see [When an exercise doesn't resolve](#when-an-exercise-doesnt-resolve).

### Muscles

`primary_muscles` is the focus of the exercise, `secondary_muscles` the work it also involves. Every source uses the same vocabulary, so a per-muscle rollup works across all of them — but how specific they are varies, so treat them as a grouping rather than a precise per-exercise judgement.

A whole-body lift — an olympic lift, a loaded carry, a burpee — reports **every muscle it works as primary**, with no secondaries, because no single muscle is the focus.

### `unknown` and `null` mean different things

`set_type`, `load_type`, `source` and the muscle lists report `unknown` when the source said something that couldn't be resolved. `null` means it said nothing at all. The distinction matters when you're deciding whether to show a gap or a question mark — and for `load_type`, whether to count the set at all.

### Values you don't recognise are expected

Movements, targets, muscles and the modifier axes are **open vocabularies** — new values are added as sources are onboarded. Treat one you don't recognise as unknown rather than switching exhaustively over the ones you know: a new member is not a breaking change, and code that rejects one will break on the next integration Terra ships.

Muscles carry one extra caveat: a new member can shift a per-muscle total even though it never breaks parsing. If you aggregate per muscle, see the [data models reference](https://docs.tryterra.co/reference/health-and-fitness-api/data-models) for which members group together.

Full field lists, types and the complete vocabularies: [Data models](https://docs.tryterra.co/reference/health-and-fitness-api/data-models).
