Strength training data
Read per-set strength training data — exercises, sets, reps and load — from the activity payload
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 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 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.
Garmin
The usual Garmin connection
Detected on the watch, so source is device_detected — see 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:
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:
end_date is exclusive. See Requesting historical data for the full endpoint.
Reading load correctly
weight_kg is meaningless on its own. Always read it with load_type:
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.
repsis per side for unilateral movements, so checkmodifiers.lateralitybefore 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:
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:
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, andweight_kgisnullmore 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.
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 for which members group together.
Full field lists, types and the complete vocabularies: Data models.
Last updated
Was this helpful?