Skip to content

Create a client on the current team

POST
/api/clients/
curl -X POST https://api.ai.aikynetix.app/api/clients/ \
-H 'Authorization: Bearer aik_yourPartnerTokenHere' \
-H 'Content-Type: application/json' \
-d '{
"display_name": "Anna Petrova",
"gender": "female",
"height_cm": 168,
"weight_kg": 58,
"external_uid": "club-roster-id-2419"
}'

Creates a new Client (athlete) under the signed-in coach’s active team. display_name is required; everything else (email, gender, height_cm, weight_kg, leg_length_cm, shoe_size, external_uid) is optional but enriches body-aware metric adjustments downstream — a Client without height_cm and weight_kg falls back to default green zones, which are noticeably less accurate. external_uid is your own stable id for cross-system joins; we never interpret it. Returns the created Client.

402 — client_limit_reached when the team has already filled plan.max_client_profiles. Free’s (1) cap makes this fire on the second create; the payload shape is identical to the other billing gates (quota / suspended / seat) — see PaywallErrorResponse.

403 when the caller has no active team selected (a transient state, e.g. mid team-switch or just after the active team was deleted).

object
display_name
required

Athlete’s display name as the coach refers to them. Required.

string
>= 1 characters <= 200 characters
email
Any of:
string format: email
<= 254 characters
gender

male / female / nonbinary / empty string — case and surrounding whitespace are normalised, anything else is a 400. Drives gender-specific green-zone overrides on running cadence, stride length, jump power, and other normative metrics. Leave blank if unknown — you’ll get the all-genders defaults.

string
<= 16 characters
date_of_birth
required

ISO date (YYYY-MM-DD). Required on write; reads back null on athletes migrated before the requirement. Drives age-band-aware green zones — a masters runner and a junior are judged against age-appropriate bands. Stored as the date, not the age, so it never goes stale.

string format: date
skill_level
One of:
  • beginner - Beginner
  • intermediate - Intermediate
  • advanced - Advanced
string
Allowed values: beginner intermediate advanced
height_cm
required

Standing height in centimetres (55-260). Required on write; reads back null on athletes migrated before the requirement. Drives proportional scaling of stride length, vertical oscillation, step width, and trunk sway green zones (legacy rows without it fall back to a 170 cm reference).

integer
<= 32767
weight_kg
required

Body weight in kilograms (20-400). Required on write; reads back null on athletes migrated before the requirement — drives force / power / impulse metrics on jump and agility activities (legacy rows without it fall back to 75 kg, which can introduce 20-30% error on those readings).

number format: double
leg_length_cm

Optional. Leg length in centimetres (35-150). Used only by specific running analyzers when present; safe to leave null.

number format: double
nullable
shoe_size

Free-text shoe size (e.g. 42 EU, 9 US). Coach reference only.

string
<= 16 characters
avatar_url
Any of:
string format: uri
<= 200 characters
dominant_hand
One of:
  • right - Right
  • left - Left
  • ambidextrous - Ambidextrous
string
Allowed values: right left ambidextrous
position
One of:
  • quarterback - Quarterback
  • running_back - Running Back
  • wide_receiver - Wide Receiver
  • tight_end - Tight End
  • offensive_line - Offensive Line
  • defensive_line - Defensive Line
  • linebacker - Linebacker
  • defensive_back - Defensive Back
string
Allowed values: quarterback running_back wide_receiver tight_end offensive_line defensive_line linebacker defensive_back
injury_history

List of prior / ongoing injury entries. Each item: { id, bodyPartId, label, side?, date?, severity?, note? } (mirrors the InjuryHistoryEditor item shape; date is yyyy-mm-dd or the sentinel ongoing). Stored verbatim so a Solutions InjuryHistoryBlock renders it with no transform.

workstation_setup

Reusable Workspace Wellness workstation setup — { posture, measurements_cm: { desktop_height, seat_top_height?, seat_pan_depth?, back_rest_height?, eye_level_height, screen_top_height } } (mirrors WorkstationSetupSerializer; seat fields required only when posture is seated). The ergonomics survey step pre-fills from this so a returning subject doesn’t re-measure their desk; each session snapshots its own point-in-time copy. Empty object {} means unset. See #303 / #530.

notes

Coach’s free-text notes on the athlete — the general-purpose field that replaced the narrow sport_type / nature_of_work questions (#926). Bounded to 2000 chars.

string
<= 2000 characters
Examples

All optional fields populated (best metric accuracy)

{
"display_name": "Anna Petrova",
"email": "anna@example.com",
"gender": "female",
"height_cm": 168,
"weight_kg": 58,
"shoe_size": "38 EU",
"external_uid": "club-roster-id-2419"
}
Media typeapplication/json
object
id
required

Server-assigned UUID for the athlete.

string format: uuid
display_name
required

Athlete’s display name as the coach refers to them. Required.

string
<= 200 characters
email
Any of:
string format: email
<= 254 characters
gender

male / female / nonbinary / empty string — case and surrounding whitespace are normalised, anything else is a 400. Drives gender-specific green-zone overrides on running cadence, stride length, jump power, and other normative metrics. Leave blank if unknown — you’ll get the all-genders defaults.

string
<= 16 characters
date_of_birth
required

ISO date (YYYY-MM-DD). Required on write; reads back null on athletes migrated before the requirement. Drives age-band-aware green zones — a masters runner and a junior are judged against age-appropriate bands. Stored as the date, not the age, so it never goes stale.

string format: date
nullable
age
required

Whole-years chronological age, computed live from date_of_birth (never stored). null when no DOB is set. Drives age-band-aware metric green zones.

integer
nullable
age_band
required

Demographic age band derived live from ageunder_18 / 18_34 / 35_49 / 50_64, then five-year bands above 65 (65_69 / 70_74 / 75_79 / 80_84 / 85_plus), or "" when no DOB is set. The same slice the green-zone resolver and the threshold configurator use; band cutoffs are server-owned so clients never re-derive them (#1406).

string
skill_level
One of:
  • beginner - Beginner
  • intermediate - Intermediate
  • advanced - Advanced
string
Allowed values: beginner intermediate advanced
height_cm
required

Standing height in centimetres (55-260). Required on write; reads back null on athletes migrated before the requirement. Drives proportional scaling of stride length, vertical oscillation, step width, and trunk sway green zones (legacy rows without it fall back to a 170 cm reference).

integer
nullable <= 32767
weight_kg
required

Body weight in kilograms (20-400). Required on write; reads back null on athletes migrated before the requirement — drives force / power / impulse metrics on jump and agility activities (legacy rows without it fall back to 75 kg, which can introduce 20-30% error on those readings).

number format: double
nullable
leg_length_cm

Optional. Leg length in centimetres (35-150). Used only by specific running analyzers when present; safe to leave null.

number format: double
nullable
shoe_size

Free-text shoe size (e.g. 42 EU, 9 US). Coach reference only.

string
<= 16 characters
avatar_url
Any of:
string format: uri
<= 200 characters
dominant_hand
One of:
  • right - Right
  • left - Left
  • ambidextrous - Ambidextrous
string
Allowed values: right left ambidextrous
position
One of:
  • quarterback - Quarterback
  • running_back - Running Back
  • wide_receiver - Wide Receiver
  • tight_end - Tight End
  • offensive_line - Offensive Line
  • defensive_line - Defensive Line
  • linebacker - Linebacker
  • defensive_back - Defensive Back
string
Allowed values: quarterback running_back wide_receiver tight_end offensive_line defensive_line linebacker defensive_back
injury_history

List of prior / ongoing injury entries. Each item: { id, bodyPartId, label, side?, date?, severity?, note? } (mirrors the InjuryHistoryEditor item shape; date is yyyy-mm-dd or the sentinel ongoing). Stored verbatim so a Solutions InjuryHistoryBlock renders it with no transform.

workstation_setup

Reusable Workspace Wellness workstation setup — { posture, measurements_cm: { desktop_height, seat_top_height?, seat_pan_depth?, back_rest_height?, eye_level_height, screen_top_height } } (mirrors WorkstationSetupSerializer; seat fields required only when posture is seated). The ergonomics survey step pre-fills from this so a returning subject doesn’t re-measure their desk; each session snapshots its own point-in-time copy. Empty object {} means unset. See #303 / #530.

notes

Coach’s free-text notes on the athlete — the general-purpose field that replaced the narrow sport_type / nature_of_work questions (#926). Bounded to 2000 chars.

string
<= 2000 characters
external_uid
required

Your own stable id for this athlete in your external system (athlete-management software, club ID, etc.). We never interpret this; useful for cross-system joins. Read-only here — set at create time only.

string
session_count
required

Number of sessions tied to this client. Read-only; computed live. The web’s delete-confirm dialog uses this to spell out how much history a delete will wipe.

integer
group_ids
required

UUIDs of the ClientGroups this athlete belongs to (#545) — many-to-many, so an athlete can be in several. Drives the group chips + filter on the Clients page; mutate via the /api/client-groups/<id>/members/ endpoints, never here.

Array<string>
created_at
required

ISO-8601 UTC timestamp of client creation.

string format: date-time
updated_at
required

ISO-8601 UTC timestamp of the last mutation.

string format: date-time
Examples
ExampleCreated
{
"id": "f6d52e64-3aa3-4a26-bac2-0fc9d718448d",
"display_name": "Anna Petrova",
"email": "anna@example.com",
"gender": "female",
"height_cm": 168,
"weight_kg": 58,
"leg_length_cm": null,
"shoe_size": "38 EU",
"avatar_url": "https://media.ai.aikynetix.app/teams/…/clients/…/avatar-a1b2c3d4.jpg",
"external_uid": "club-roster-id-2419",
"session_count": 0,
"created_at": "2026-04-29T10:00:00Z",
"updated_at": "2026-04-29T10:00:00Z"
}
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
Example400—ValidationError

400 — Validation error

{
"display_name": [
"This field is required."
]
}
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

One of five canonical strings. The front-end paywall switches copy + CTA on this value (exact string equality — do not localise). quota_exhausted / seat_limit_reached / client_limit_reached / feature_not_in_plan lead to an upgrade CTA; subscription_suspended leads to a Billing Portal CTA so the buyer can update their payment method.

  • quota_exhausted - Session quota for the period reached 0
  • subscription_suspended - Stripe dunning in flight (past_due / unpaid)
  • seat_limit_reached - Adding another team member would exceed plan.max_seats
  • client_limit_reached - Adding another Client would exceed plan.max_client_profiles
  • feature_not_in_plan - The tier does not include this whole feature surface
string
Allowed values: quota_exhausted subscription_suspended seat_limit_reached client_limit_reached feature_not_in_plan
reset_at
required

ISO-8601 UTC timestamp the team’s current period ends. Null when the team has no live subscription (legacy un-migrated row). On Free + paid this is the renewal moment; quota refreshes to exactly plan.session_quota (NOT additive — leftover sessions don’t roll over).

string format: date-time
nullable
current_plan
required

The active plan’s role string (free, starter, professional, organization, strategic). Same value as current_role — kept as two separate fields for forward-compatibility with a future named-plan split where current_plan could carry an SKU and current_role the tier label.

string
current_role
required

The active plan’s role string. See current_plan.

string
feature

Which feature surface is locked. Present ONLY on feature_not_in_plan, so one modal can name what’s gated without a detail string per feature (#773). The only value today is agents (aikynetix/agents/services.py::AGENTS_FEATURE).

string
Examples
Example402—ClientLimitReachedOnFree

Free team's `max_client_profiles=1` is already filled

{
"detail": "client_limit_reached",
"reset_at": "2026-06-01T00:00:00Z",
"current_plan": "free",
"current_role": "free"
}
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"
}