Med spas
For medical spas: pre-treatment clearances, treatments recorded against the member, adverse events reported and followed, re-treatment due dates and the aftercare photos the member chose to share. Webhook: medspa.adverse_event_reported. The medical director's GLP-1 oversight roster: patients on a GLP-1 who share their medication data, each finding quoting the FDA label with its citation (pending clinician sign-off; never a dose suggestion).
- Who calls it
- Core for Med spas
- Before production
- Medical-spa organisations (a treating practice) with a BAA on file.
- Sandbox
- Build against synthetic members first. What each sandbox holds
Markets that use this family
- Med spas (core)
Scopes
| Scope | Allows |
|---|---|
medspa.aftercare.read | Read aesthetic treatments with their aftercare check-in schedule and alerts, adverse events, and the retreatment-due list on your practice's own intervals (treating practices, BAA on file). |
medspa.aftercare.write | Record an aesthetic treatment (its aftercare check-ins are scheduled) and an adverse event for a patient of your practice (treating practices, BAA on file). |
medspa.clearance.read | Read your practice's treatment-clearance intakes: the screen's flags with their reason and source, and the supervising provider's decision and expiry (treating practices that offer clearance, BAA on file). |
medspa.glp1_oversight.read | Read your medical director's GLP-1 oversight roster: patients on a GLP-1 / GIP medicine whose medication data they share with you, the therapy as recorded, last injection logged, weight trend, latest labs, diabetes monitoring status and each finding with its verbatim FDA label citation (pending clinician sign-off); never a dose suggestion or a prescribing recommendation (treating practices, BAA on file). |
medspa.photos.read | Read a patient's before / after photo timeline as metadata (view, phase, area, date, consent flags); image files are not served through the API (treating practices). |
Endpoints
Treatment-clearance intakes
med_spa_clearance: each intake's category, status (awaiting_review | reviewed | expired), the screen's contraindication / precaution counts and the supervising provider's decision with its expiry (a practice setting). The screen is never a clearance: only a provider's decision makes one.
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/clearances" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/medspa/clearances",
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 url = new URL(`${BASE}/v1/medspa/clearances`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
One clearance intake: flags, reasons, sources and the decision
Every flag the screen raised (item, severity, reason, the primary source it rests on with its verbatim quote, and any disagreeing source), the decision, its note and the deciding clinician as a staff_ref. The member's raw answers and medication details stay in the app.
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/clearances/$CLEARANCE_REF" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
clearance_ref = os.environ["CLEARANCE_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/clearances/{clearance_ref}",
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 clearance_ref = process.env.CLEARANCE_REF;
const url = new URL(`${BASE}/v1/medspa/clearances/${encodeURIComponent(clearance_ref)}`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
A patient's aesthetic treatments and aftercare plans
Each treatment (category, areas, date, product, quantity, lot, treating clinician as staff_ref, consent on file) with its aftercare plan: the scheduled check-ins (the practice's own days for the category, else Norvya's default 1 / 3 / 14 — a convention, not a clinical standard), the warning signs the patient reported and any alert (never closed by time).
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/members/$MEMBER_REF/treatments" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/members/{member_ref}/treatments",
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 url = new URL(`${BASE}/v1/medspa/members/${encodeURIComponent(member_ref)}/treatments`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
Record an aesthetic treatment (schedules its aftercare check-ins)
med_spa_aftercare.create_treatment: its validation (category, areas, date within the last 365 days and not in the future, units), then the check-ins are scheduled and the patient is reminded by Norvya's generic push when one is due.
Parameters
Request body
Content type: application/json (required)
neurotoxin | dermal_filler | laser_energy | chemical_peel | microneedling | iv_therapy | other
YYYY-MM-DD, within the last 365 days (default today).
units | ml | syringes | vials | passes | sessions | other
staff_ref of the treating clinician (GET /v1/panel/clinicians)
Errors specific to this call
365daysAll responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/medspa/members/$MEMBER_REF/treatments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"category": "string",
"areas": [
"string"
],
"treated_on": "string",
"product_name": "string",
"quantity": 0.0,
"quantity_unit": "string",
"lot_number": "string",
"provider": "string",
"notes": "string"
}'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/medspa/members/{member_ref}/treatments",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"category": "string",
"areas": [
"string",
],
"treated_on": "string",
"product_name": "string",
"quantity": 0.0,
"quantity_unit": "string",
"lot_number": "string",
"provider": "string",
"notes": "string",
},
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/medspa/members/${encodeURIComponent(member_ref)}/treatments`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"category": "string",
"areas": [
"string"
],
"treated_on": "string",
"product_name": "string",
"quantity": 0.0,
"quantity_unit": "string",
"lot_number": "string",
"provider": "string",
"notes": "string"
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
One treatment and its aftercare plan
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/treatments/$TREATMENT_REF" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
treatment_ref = os.environ["TREATMENT_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/treatments/{treatment_ref}",
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 treatment_ref = process.env.TREATMENT_REF;
const url = new URL(`${BASE}/v1/medspa/treatments/${encodeURIComponent(treatment_ref)}`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
Adverse events after aesthetic treatments
Confirmed events (recorded by staff or through the API), drafts a patient's warning-sign check-in created (a clinician confirms or dismisses them in the app) and dismissed drafts with the reason. from / to filter on the date the event was recorded (default the last 90 days).
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/adverse-events" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/medspa/adverse-events",
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 url = new URL(`${BASE}/v1/medspa/adverse-events`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
One adverse event
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/adverse-events/$EVENT_REF" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
event_ref = os.environ["EVENT_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/adverse-events/{event_ref}",
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 event_ref = process.env.EVENT_REF;
const url = new URL(`${BASE}/v1/medspa/adverse-events/${encodeURIComponent(event_ref)}`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
Report an adverse event after an aesthetic treatment
med_spa_aftercare.create_event: recorded as confirmed (the practice reporting it), its vocabulary validated by the engine, optionally tied to one of the patient's treatments here. Fires medspa.adverse_event_reported to your other subscribed endpoints.
Parameters
Request body
Content type: application/json (required)
vascular_occlusion | visual_disturbance | distant_toxin_spread | eyelid_or_brow_droop | allergic_reaction | infection | burn_or_blister | nodule_or_lump | pigment_change | scarring | bruising_or_swelling | cold_sore_outbreak | other
mild | moderate | severe
resolved | resolving | not_resolved | resolved_with_sequelae | unknown
All responses
201Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/medspa/members/$MEMBER_REF/adverse-events" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type": "string",
"severity": "string",
"description": "string",
"onset_on": "string",
"actions_taken": "string",
"outcome": "string",
"follow_up_on": "string",
"treatment_ref": "string"
}'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/medspa/members/{member_ref}/adverse-events",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"event_type": "string",
"severity": "string",
"description": "string",
"onset_on": "string",
"actions_taken": "string",
"outcome": "string",
"follow_up_on": "string",
"treatment_ref": "string",
},
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/medspa/members/${encodeURIComponent(member_ref)}/adverse-events`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"event_type": "string",
"severity": "string",
"description": "string",
"onset_on": "string",
"actions_taken": "string",
"outcome": "string",
"follow_up_on": "string",
"treatment_ref": "string"
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
Patients whose retreatment may be due, on your practice's own intervals
med_spa_retreatment.compute_items: the latest active treatment per patient, category and area, due on the interval YOUR practice saved (a practice setting, not clinical guidance; a category without one is never due). reminders_opted_out = the patient asked this practice for no retreatment reminders.
Parameters
comma list of not_due | due_soon | due | overdue (default due_soon,due,overdue)
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/retreatment-due" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/medspa/retreatment-due",
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 url = new URL(`${BASE}/v1/medspa/retreatment-due`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
A patient's photo timeline (metadata only)
med_spa_photos: each active photo's view, phase, area, date, the treatment it belongs to, and its consent flags (member_visible, marketing_consent: only the patient grants marketing). Image bytes are not served (image: null) — Norvya has no signed short-lived URL for aesthetic photos, and they are never sent to a model.
Parameters
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/members/$MEMBER_REF/photos" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/members/{member_ref}/photos",
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 url = new URL(`${BASE}/v1/medspa/members/${encodeURIComponent(member_ref)}/photos`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
GLP-1 oversight roster for your medical director
glp1_oversight.roster, the staff oversight screen's own engine: each patient on a GLP-1 / GIP medicine whose medication data they share with your practice (the share_medications switch; others are not listed), the therapy and dose as recorded, start date, last injection logged, weight trend, latest HbA1c / eGFR / creatinine, diabetes monitoring status and the live findings, ranked as on the staff screen. Each finding quotes the FDA prescribing information verbatim with its URL (citations[]; a plateau is a Norvya convention and says so). Every rule is pending clinician sign-off. Never a dose suggestion or a prescribing recommendation.
Parameters
urgent | high | medium | low: only members with a finding at it
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/glp1-oversight" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/medspa/glp1-oversight",
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 url = new URL(`${BASE}/v1/medspa/glp1-oversight`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
One patient's GLP-1 oversight row
The same row as on the roster. 404 member_not_on_glp1_roster when the patient is not on it — not on a GLP-1, or not sharing medication data with your practice: the two read the same, on purpose.
Parameters
Errors specific to this call
404member_not_on_glp1_rosterAll responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/medspa/glp1-oversight/$MEMBER_REF" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
member_ref = os.environ["MEMBER_REF"]
resp = requests.get(
f"{BASE}/v1/medspa/glp1-oversight/{member_ref}",
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 url = new URL(`${BASE}/v1/medspa/glp1-oversight/${encodeURIComponent(member_ref)}`);
const res = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());Try it in your sandbox
Webhook events
Payloads carry no health data, only a reference to fetch with your token. Each event needs its scope as well as webhooks.manage. Signing and retries
| Event | Scope | When |
|---|---|---|
medspa.adverse_event_reported | medspa.aftercare.read | An aesthetic adverse event was recorded at the practice (by staff, through the Partner API, or as a draft from a patient's warning-sign check-in). |
Generated from the Partner API router and developers_catalog.py. The router docstring is the source of each description.