Skip to content

Create a team threshold override

POST
/api/thresholds/
curl --request POST \
--url http://localhost:8000/api/thresholds/ \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "activity": "running", "exercise": "Treadmill", "metric_key": "cadence", "gender": "", "min_good": 175, "max_good": 185, "reason": "Club-specific target after Sibhashri review 2026-05" }'

Any team member; audited. Persists a new team-scoped MetricThreshold row for the given (activity, exercise, metric_key, gender, segment) tuple — overriding the system / MetricSpec default for the team. Writes a MetricThresholdAudit entry (action=create) in the same transaction; reason is required (sign-off note).

Returns 409 if a team override already exists for that tuple (use PATCH instead). Returns 400 on min ≥ max, unknown metric_key, or a band that overlaps an ordered metric pair — walking stride_length must stay strictly above the stance_length band and vice versa, since a full gait cycle always covers more ground than a single stance (#1236).

The pair check has two shapes. An overlap with the band that applies at the same demographic / exercise slice is keyed on the offending bound (min_good / max_good) so a client can point at the field. An overlap only visible in a slice the row inherits into — e.g. a gender=female stride row against a base stance_length row — is keyed on detail, because the conflicting row is one this request never touched. Either way the row is not stored.

norms on the returned row is ALWAYS null — the Movement-Norms reference band is a list-shape concern resolved per requested slice (#1406), and the page refetches the list after a write. Read the band from GET /api/thresholds/, not from this response.

POST requires the natural-key fields so we can build a fresh row. Inherits validation; tightens required-ness via __init__.

object
activity
required
  • running - Running
  • walking - Walking
  • jump - Vertical Jump
  • weightlifting - Weightlifting
  • mobility - Mobility Assessment
  • workspace_wellness - Workspace Wellness
  • agility - Agility
  • fencing - Fencing
  • cycling - Cycling
  • tennis - Tennis
  • padel - Padel Tennis
  • bowling - Bowling
  • baseball - Baseball
  • football - Football
  • badminton - Badminton
  • cricket - Cricket
  • basketball - Basketball
  • scuba_diving - Scuba Diving
  • ice_hockey - Ice Hockey
  • american_football - American Football
  • fri - Falls Risk Indicator
string
Allowed values: running walking jump weightlifting mobility workspace_wellness agility fencing cycling tennis padel bowling baseball football badminton cricket basketball scuba_diving ice_hockey american_football fri
exercise
string
nullable
metric_key
required
string
>= 1 characters
gender
One of:
  • male - Male
  • female - Female
  • nonbinary - Non-binary
  • `` -
string
Allowed values: male female nonbinary
segment
string
nullable
age_band
One of:
  • under_18 - Under 18
  • 18_34 - 18–34
  • 35_49 - 35–49
  • 50_64 - 50–64
  • 65_69 - 65–69
  • 70_74 - 70–74
  • 75_79 - 75–79
  • 80_84 - 80–84
  • 85_plus - 85+
  • `` -
string
Allowed values: under_18 18_34 35_49 50_64 65_69 70_74 75_79 80_84 85_plus
skill_level
One of:
  • beginner - Beginner
  • intermediate - Intermediate
  • advanced - Advanced
string
Allowed values: beginner intermediate advanced
min_good
number format: double
nullable
max_good
number format: double
nullable
higher_is_better
boolean
nullable
unit
string
reason
required
string
>= 1 characters
notes
string
Examples
ExampleOverrideCadenceForTreadmillRunning

Override cadence for treadmill running

{
"activity": "running",
"exercise": "Treadmill",
"metric_key": "cadence",
"gender": "",
"min_good": 175,
"max_good": 185,
"reason": "Club-specific target after Sibhashri review 2026-05"
}
Media typeapplication/json

Read shape — either a materialised team row (inherited=False, id populated) or a synthetic catalog entry sourced from MetricSpec (inherited=True, id=None). Plain Serializer rather than ModelSerializer because inherited rows have no DB backing — the container POSTs to materialise on first edit.

object
id
required
string
inherited
required
boolean
activity
required
string
exercise
required
string
nullable
metric_key
required
string
metric_label
required
string
unit
required
string
higher_is_better
required
boolean
nullable
min_good
required
number format: double
nullable
max_good
required
number format: double
nullable
is_height_scaled
required
boolean
default_min_good
required
number format: double
nullable
default_max_good
required
number format: double
nullable
norms
required
object
min
required
number format: double
nullable
max
required
number format: double
nullable
evidence_basis
required
string
source
required
string
scope
required
  • cohort - Cohort
  • base - Base
string
Allowed values: cohort base
overrides
required
Array<object>
object
kind
required
  • gender - gender
  • age_band - age_band
  • skill_level - skill_level
  • height_scaled - height_scaled
  • segment - segment
string
Allowed values: gender age_band skill_level height_scaled segment
label
required
string
detail
required
object
key
additional properties
number format: double
nullable
slice

The demographic slice an override row lives in — the FE’s jump target when the coach clicks the badge to land on that cohort. Empty strings are the “all” sentinel for each dimension.

object
gender
required
string
age_band
required
string
skill_level
required
string
observed_distribution
required
object
bins
required
Array<object>
object
bin_min
required
number
bin_max
required
number
count
required
integer
total_sessions
required
integer
axis_min
required
number
axis_max
required
number
session_window_days
required
integer
Examples
ExamplePersistedRow

Persisted row

{
"id": "7b3d9e1a-2c4f-4a6b-8d0e-1f2a3b4c5d6e",
"inherited": false,
"activity": "running",
"exercise": "Treadmill",
"metric_key": "cadence",
"unit": "spm",
"higher_is_better": false,
"min_good": 175,
"max_good": 185,
"is_height_scaled": false,
"norms": {
"min": 170,
"max": 180,
"evidence_basis": "validated",
"source": "Running cadence norms (Sport Science Insider)",
"scope": "base"
},
"overrides": [],
"observed_distribution": {
"bins": [],
"total_sessions": 0,
"axis_min": 0,
"axis_max": 1,
"session_window_days": 30
}
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examples

400 — band overlaps an ordered metric pair

{
"min_good": [
"Stride Length min_good (0.6) must be strictly above the Stance Length max_good (0.9) that applies to this team. A full gait cycle always covers more ground than a single stance, because a foot is planted for less time than the cycle it belongs to — the two bands cannot overlap."
]
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examplegenerated
{
"detail": "example"
}
Media typeapplication/json
object
detail
required

Human-readable message, or a stable machine code for the cases a client branches on. The standard envelope for 400 (validation — a field-keyed object may appear instead), 401 (missing / invalid credentials), 403 (authenticated but not permitted), and 404 (absent — cross-team records are collapsed to 404 so the API never leaks the existence of another team’s data).

string
Examplegenerated
{
"detail": "example"
}
Media typeapplication/json
object
detail
required

409 — the request conflicts with current state. detail is a stable code (e.g. reanalyze_in_flight, already_on_plan, email_in_use). Some 409s carry extra context fields alongside detail; those are documented on the specific endpoint that emits them.

string
Examples
Example409—OverrideAlreadyExists(usePATCH)

409 — Override already exists (use PATCH)

{
"detail": "threshold override already exists for this metric"
}