Member data in: observations and workouts
Send what your own system measured for a member, keyed by your id so a resend replaces and a delete retracts: vitals, body measurements and lab results (practices, with a BAA; recorded with your organisation as the source and coded to LOINC / UCUM where Norvya is sure of the code), and completed workouts (gyms and fitness apps) that strength progress, weekly sets and readiness then read. Every value lands in the member's provenance-bearing observation record, never as a silent overwrite of something the member entered.
- Who calls it
- Core for Fitness chains; useful for Concierge doctors, Independent practices, EMR vendors, Hospitals and health systems, Rehab & nutrition practices
- Before production
- No gate of its own; your market's “Before production” applies.
- Sandbox
- Build against synthetic members first. What each sandbox holds
- Standards
- LOINC / UCUM where Norvya is sure of the code
Markets that use this family
- Concierge doctors (useful)
- Independent practices (useful)
- Fitness chains (core)
- EMR vendors (useful)
- Hospitals and health systems (useful)
- Rehab & nutrition practices (useful)
Scopes
| Scope | Allows |
|---|---|
observations.write | Write measurements to a member's own record: vitals, lab results and body composition (practices with a BAA on file), or body composition and the vitals a gym measures (gyms; never labs). Idempotent per item by your external_id; units converted, implausible values refused; DELETE retracts only your own item. |
workouts.write | Write completed training sessions (exercises, sets, reps, load, RPE, duration) to a member's record, so strength progress, weekly sets and readiness see them (gyms, fitness and personal-training organisations only). |
Endpoints
Write measurements to a member's record
Vitals, lab results and body composition, one outcome per item (created | duplicate | rejected with a machine code). Practices (BAA on file): every type, recorded as your practice's own record. Gyms: body composition and weight / height / heart rate / blood pressure, while the member shares body measurements with you; never labs. Units are converted, never assumed; implausible values are refused. Idempotent per item by external_id.
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
Request body
Content type: application/json (required)
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/members/$MEMBER_REF/observations" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"external_id": "VS-2026-10-14-0001",
"type": "vital",
"code": "blood_pressure",
"value": 128,
"value_secondary": 82,
"unit": "mmHg",
"effective_at": "2026-10-14T09:30:00-04:00",
"device": "InBody 570",
"name": "LDL",
"loinc": "13457-7",
"collected_at": "2026-10-13T08:05:00-04:00",
"reference_low": 0.0,
"reference_high": 0.0,
"reference_text": "<100 mg/dL",
"interpretation": "string",
"lab_name": "Example Reference Laboratory",
"weight": {
"value": 82.4,
"unit": "kg"
},
"body_fat_pct": 21.4,
"skeletal_muscle_mass": {
"value": 82.4,
"unit": "kg"
},
"bone_mass": {
"value": 82.4,
"unit": "kg"
},
"body_water_pct": 0.0,
"visceral_fat": 9,
"smi": 0.0,
"ecw_tbw": 0.0
}
]
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.post(
f"{BASE}/v1/members/{member_ref}/observations",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"items": [
{
"external_id": "VS-2026-10-14-0001",
"type": "vital",
"code": "blood_pressure",
"value": 128,
"value_secondary": 82,
"unit": "mmHg",
"effective_at": "2026-10-14T09:30:00-04:00",
"device": "InBody 570",
"name": "LDL",
"loinc": "13457-7",
"collected_at": "2026-10-13T08:05:00-04:00",
"reference_low": 0.0,
"reference_high": 0.0,
"reference_text": "<100 mg/dL",
"interpretation": "string",
"lab_name": "Example Reference Laboratory",
"weight": {
"value": 82.4,
"unit": "kg",
},
"body_fat_pct": 21.4,
"skeletal_muscle_mass": {
"value": 82.4,
"unit": "kg",
},
"bone_mass": {
"value": 82.4,
"unit": "kg",
},
"body_water_pct": 0.0,
"visceral_fat": 9,
"smi": 0.0,
"ecw_tbw": 0.0,
},
],
},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/observations`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"items": [
{
"external_id": "VS-2026-10-14-0001",
"type": "vital",
"code": "blood_pressure",
"value": 128,
"value_secondary": 82,
"unit": "mmHg",
"effective_at": "2026-10-14T09:30:00-04:00",
"device": "InBody 570",
"name": "LDL",
"loinc": "13457-7",
"collected_at": "2026-10-13T08:05:00-04:00",
"reference_low": 0.0,
"reference_high": 0.0,
"reference_text": "<100 mg/dL",
"interpretation": "string",
"lab_name": "Example Reference Laboratory",
"weight": {
"value": 82.4,
"unit": "kg"
},
"body_fat_pct": 21.4,
"skeletal_muscle_mass": {
"value": 82.4,
"unit": "kg"
},
"bone_mass": {
"value": 82.4,
"unit": "kg"
},
"body_water_pct": 0.0,
"visceral_fat": 9,
"smi": 0.0,
"ecw_tbw": 0.0
}
]
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"created": 0,
"duplicates": 0,
"rejected": 0,
"items": [
{
"external_id": "string",
"type": "string",
"status": "created",
"code": "unit_unrecognised",
"metrics": [
"string"
]
}
],
"source": "practice_emr"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Retract one of your own measurement items
Removes the rows YOUR item created; their observations are marked entered-in-error in Norvya's ledger. Another source's data is never touched. The external_id is not reusable afterwards.
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X DELETE "https://norvya.fly.dev/v1/members/$MEMBER_REF/observations/$EXTERNAL_ID" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
external_id = os.environ["EXTERNAL_ID"]
resp = requests.delete(
f"{BASE}/v1/members/{member_ref}/observations/{external_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const external_id = process.env.EXTERNAL_ID;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/observations/${encodeURIComponent(external_id)}`);
const res = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"external_id": "string",
"status": "retracted",
"already_retracted": false,
"rows_removed": 0
}The response is the shape from the schema, not live data.
Try it in your sandbox
Write completed training sessions to a member's record
Exercises, sets, reps, load and RPE, stored as a logged session so the member's strength progress (estimated 1RM), weekly hard sets and training readiness include it. Gyms, fitness and personal-training organisations, while the member shares activity with you.
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
Request body
Content type: application/json (required)
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/members/$MEMBER_REF/workouts" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"external_id": "GYM-SESSION-88412",
"started_at": "2026-10-14T18:00:00-04:00",
"duration_minutes": 55,
"workout_type": "string",
"session_rpe": 7,
"device": "string",
"exercises": [
{}
]
}
]
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.post(
f"{BASE}/v1/members/{member_ref}/workouts",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"items": [
{
"external_id": "GYM-SESSION-88412",
"started_at": "2026-10-14T18:00:00-04:00",
"duration_minutes": 55,
"workout_type": "string",
"session_rpe": 7,
"device": "string",
"exercises": [
{},
],
},
],
},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/workouts`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"items": [
{
"external_id": "GYM-SESSION-88412",
"started_at": "2026-10-14T18:00:00-04:00",
"duration_minutes": 55,
"workout_type": "string",
"session_rpe": 7,
"device": "string",
"exercises": [
{}
]
}
]
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"created": 0,
"duplicates": 0,
"rejected": 0,
"items": [
{
"external_id": "string",
"type": "string",
"status": "created",
"code": "unit_unrecognised",
"metrics": [
"string"
]
}
],
"source": "practice_emr"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Retract one of your own workout items
Parameters
EMR vendors also pass vendor (the practice) with a vendor-issued token. EMR vendor tokens
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X DELETE "https://norvya.fly.dev/v1/members/$MEMBER_REF/workouts/$EXTERNAL_ID" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
external_id = os.environ["EXTERNAL_ID"]
resp = requests.delete(
f"{BASE}/v1/members/{member_ref}/workouts/{external_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const member_ref = process.env.MEMBER_REF;
const external_id = process.env.EXTERNAL_ID;
const url = new URL(`${BASE}/v1/members/${encodeURIComponent(member_ref)}/workouts/${encodeURIComponent(external_id)}`);
const res = await fetch(url, {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"member_ref": "string",
"external_id": "string",
"status": "retracted",
"already_retracted": false,
"rows_removed": 0
}The response is the shape from the schema, not live data.
Try it in your sandbox
Generated from the Partner API router and developers_catalog.py. The router docstring is the source of each description.