Events (webhooks)
Signed notifications: a member newly at high risk of leaving (gyms), a worklist run finishing, a patient newly needing attention, an import completing, a population export ready, a registry acknowledgement or a Reportability Response arriving, a patient's care-management time reaching a program's first published mark, an appointment booked or cancelled in Norvya (practices), a member joining, a member's message, a questionnaire completed, a worklist item triaged, a claims import completed, a social-needs screen completed or a community referral updated, an imported patient claiming their account, a caseload questionnaire batch dispatched, a hospital-at-home escalation, a pharmacy refill due or discharge follow-up opened, a care-site change of condition, medication incident or MAR exception, and a medical-travel inquiry, journey, journey message, flight leg, nursing visit or invoice update; and, from the specialty and programme families, a roster's enrolment numbers changing, a raised falls tier, a challenge ending, an occupational exam coming due, a med-spa adverse event, a rehab goal met, a dietitian caseload flag, a Collaborative Care review due, a lesion recheck due, a patch-test read and a hospice IDG review due. Payloads carry no health data, only a reference to fetch with your token.
- Who calls it
- Every partner: common to all markets.
- Before production
- No gate of its own; your market's “Before production” applies.
- Sandbox
- Build against synthetic members first. What each sandbox holds
Markets that use this family
- Concierge doctors (core)
- Independent practices (core)
- Fitness chains (core)
- EMR vendors (core)
- Hospitals and health systems (core)
- Practice management & billing (core)
- Medical travel desks (core)
- Pharmacies (core)
- Nursing homes & assisted living (core)
- Employers & occupational health (core)
- Behavioral health practices (core)
- Rehab & nutrition practices (core)
- Older adults & caregiving programmes (core)
- Hospice agencies (core)
- Med spas (core)
- Specialty care (core)
Scopes
| Scope | Allows |
|---|---|
webhooks.manage | Register HTTPS webhook endpoints (payloads carry no PHI). Each event also needs its own scope: worklist / attention events clinical.panel.read, import events panel.import.write, population.export_completed population.export (medical practices only), member.at_risk fitness.roster.read, public_health.iis_ack_received public_health.immunizations.read, public_health.case_report_response public_health.case_reports.read. claims.import_completed needs claims.import.write (medical practices only). member.account_claimed needs members.claim_invite (medical practices only). hospital_at_home.escalation needs hospital_at_home.read (hospitals only). hospice.idg_review_due needs hospice.read (hospice agencies only). pharmacy.refill_due and pharmacy.discharge_episode_opened need pharmacy.patients.read; care_site.change_of_condition, care_site.incident_recorded and care_site.mar_exception need care_sites.read (pharmacies and care-facility operators, their own sites). travel.journey_updated and travel.leg_status_changed need travel.journeys.read, travel.nursing_visit_recorded travel.nursing.read, travel.invoice_issued travel.billing.read (medical-travel desks only). travel.inquiry_updated needs travel.inquiries.read, travel.journey_message_posted travel.messages.read (medical-travel desks only). instrument.batch_dispatched needs instruments.request (medical practices only). roster.enrolment_summary needs members.roster.read (medical practices only). older_adults.falls_tier_raised needs older_adults.read (medical practices only). employer.challenge_completed needs employer.challenges.read and occupational.exam_due occupational.read (employers only); medspa.adverse_event_reported needs medspa.aftercare.read (treating practices only). rehab.goal_met needs rehab.read; nutrition.caseload_flagged needs nutrition.read (medical practices only). cocm.review_due needs behavioral.read (medical practices only). derm.lesion_recheck_due and derm.patch_test_read_recorded need derm.read (medical practices only). |
Endpoints
Your webhook endpoints
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/webhooks" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/webhooks",
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/webhooks`);
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());{
"webhooks": [
{
"webhook_id": "string",
"url": "string",
"events": [
"string"
],
"status": "string",
"disabled_reason": "string",
"consecutive_failures": 0,
"created_at": "string",
"last_success_at": "string",
"last_failure_at": "string",
"disabled_at": "string"
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
Register a webhook endpoint (secret shown once)
HTTPS only; the host must resolve to public addresses only (loopback, private, link-local / metadata, CGNAT and ULA ranges are refused, at registration and again at every send; redirects are never followed). Your client must also hold the scope behind each event (worklist.* / patient.attention_needed: clinical.panel.read; import.*: panel.import.write; population.export_completed: population.export — these are practice events, for medical practices only; patient.admitted / patient.discharged / patient.ed_visit: transitions.notices.read, treating practices and hospitals; transition.episode_enrolled / transition.red_flag: transitions.read, hospitals only; member.at_risk: fitness.roster.read, any organisation, so a gym can subscribe to it) — 422 webhook_event_not_allowed otherwise.
patient.admitted / .discharged / .ed_visit fire when an ADT notice is matched to one of your patients and visible to you (after a discharge, TCM asks for contact within 2 business days: GET the resource_url for the clock). transition.episode_enrolled fires when a post-discharge episode opens at your hospital; transition.red_flag when a patient's check-in trips a new red flag. All five carry member_ref.
population.export_completed fires when an export YOUR client started finishes; it is sent to your client's endpoints only.
member.at_risk fires once when a member newly enters churn level "high" (daily scan); not again until the member leaves "high" and re-enters.
Payloads carry no PHI: {event, id, created_at, resource_url, member_ref (attention, member.at_risk, patient.* and transition.* only)} — GET the resource_url with your token. Verify every delivery: `Norvya-Signature: t=<unix>, v1=<hex>` where v1 = HMAC-SHA256(secret, "<t>." + raw body); compare in constant time, reject a t more than 300 s from your clock (replay window) and de-duplicate on the Norvya-Delivery header (delivery is at-least-once). Failed deliveries retry with exponential backoff (1, 2, 4 ... min; 8 attempts); 20 consecutive failures disable the endpoint (POST /v1/webhooks/{id}/enable).
Request body
Content type: application/json (required)
https:// only; must resolve to public addresses.
Subset of: worklist.run_completed, patient.attention_needed, import.completed, import.failed, population.export_completed (medical practices), patient.admitted, patient.discharged, patient.ed_visit (treating practices and hospitals holding transitions.notices.read), transition.episode_enrolled, transition.red_flag (hospitals holding transitions.read), member.at_risk (any organisation holding fitness.roster.read)
Errors specific to this call
422webhook_event_not_allowedAll 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/webhooks" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "string",
"events": [
"string"
]
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.post(
BASE + "/v1/webhooks",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"url": "string",
"events": [
"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 url = new URL(`${BASE}/v1/webhooks`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "string",
"events": [
"string"
]
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"webhook_id": "string",
"url": "string",
"events": [
"string"
],
"status": "string",
"disabled_reason": "string",
"consecutive_failures": 0,
"created_at": "string",
"last_success_at": "string",
"last_failure_at": "string",
"disabled_at": "string",
"secret": "string",
"signature_header": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Delete a webhook endpoint
Pending deliveries are cancelled.
Parameters
All responses
204Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X DELETE "https://norvya.fly.dev/v1/webhooks/$WEBHOOK_ID" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
webhook_id = os.environ["WEBHOOK_ID"]
resp = requests.delete(
f"{BASE}/v1/webhooks/{webhook_id}",
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=30,
)
resp.raise_for_status()
print(resp.status_code)// Node.js 18+ (ES module). Keep production credentials on your server.
const BASE = "https://norvya.fly.dev";
const webhook_id = process.env.WEBHOOK_ID;
const url = new URL(`${BASE}/v1/webhooks/${encodeURIComponent(webhook_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(res.status);No response body.The response is the shape from the schema, not live data.
Try it in your sandbox
Re-enable a disabled webhook endpoint
The URL is re-checked; the failure count resets. Deliveries cancelled while it was disabled are not re-sent.
Parameters
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/webhooks/$WEBHOOK_ID/enable" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
webhook_id = os.environ["WEBHOOK_ID"]
resp = requests.post(
f"{BASE}/v1/webhooks/{webhook_id}/enable",
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 webhook_id = process.env.WEBHOOK_ID;
const url = new URL(`${BASE}/v1/webhooks/${encodeURIComponent(webhook_id)}/enable`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"webhook_id": "string",
"url": "string",
"events": [
"string"
],
"status": "string",
"disabled_reason": "string",
"consecutive_failures": 0,
"created_at": "string",
"last_success_at": "string",
"last_failure_at": "string",
"disabled_at": "string"
}The response is the shape from the schema, not live data.
Try it in your sandbox
Recent deliveries of one webhook endpoint
Newest first: status, attempts, next retry, last HTTP status / error class, and the (PHI-free) payload. Response bodies are never stored.
Parameters
pending | delivered | failed | cancelled
All responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/webhooks/$WEBHOOK_ID/deliveries" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
webhook_id = os.environ["WEBHOOK_ID"]
resp = requests.get(
f"{BASE}/v1/webhooks/{webhook_id}/deliveries",
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 webhook_id = process.env.WEBHOOK_ID;
const url = new URL(`${BASE}/v1/webhooks/${encodeURIComponent(webhook_id)}/deliveries`);
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());{
"webhook_id": "string",
"deliveries": [
{
"delivery_id": "string",
"event": "string",
"event_id": "string",
"status": "string",
"attempts": 0,
"next_attempt_at": "string",
"last_attempt_at": "string",
"last_status_code": 0,
"last_error": "string",
"delivered_at": "string",
"created_at": "string",
"payload": {}
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
Event catalogue
Every event Norvya sends, with the scope your client must also hold and the family whose resource it points to.
| Event | Scope | Family | When |
|---|---|---|---|
appointment.booked | scheduling.read | Billing & scheduling | An appointment was booked in Norvya (your own writes are never echoed). |
appointment.cancelled | scheduling.read | Billing & scheduling | An appointment was cancelled in Norvya (your own writes are never echoed). |
billing.threshold_met | billing.read | Billing & scheduling | A patient's documented care-management time (or remote-monitoring days) for one program reached the program's first published reference mark this month. |
care_site.change_of_condition | care_sites.read | Care sites | A change-of-condition report at one of your sites (a pharmacy's or a care facility's own). |
care_site.incident_recorded | care_sites.read | Care sites | An incident report or a medication incident at one of your sites. |
care_site.mar_exception | care_sites.read | Care sites | A scheduled dose recorded held or refused at one of your sites. |
claims.import_completed | claims.import.write | Claims in | A claims import you sent finished committing (sent to that client only). |
cocm.review_due | behavioral.read | Behavioral health | The practice's weekly Collaborative Care caseload review is due (about the practice, not a patient: no member_ref). |
derm.lesion_recheck_due | derm.read | Dermatology | A tracked lesion of a member who shares skin scans with the practice reached its recheck date (once per lesion per due date). |
derm.patch_test_read_recorded | derm.read | Dermatology | A read was recorded on a patch test of the practice (once per read set; never an allergen or a grade). |
employer.challenge_completed | employer.challenges.read | Employers & occupational health | A wellness challenge's window ended (once per challenge; the results are the suppressed group counts). |
hospice.idg_review_due | hospice.read | Hospice agencies | At least one patient on service at the agency has an IDG plan-of-care review due within 3 days or overdue (about the agency, not a patient: no member_ref; once per UTC day). |
hospital_at_home.escalation | hospital_at_home.read | Hospital at home, days at home & wounds | An observation round on a hospital-at-home episode scored at or above NEWS2's urgent-response threshold. |
import.completed | panel.import.write | Data in | A panel import made through the API committed. |
import.failed | panel.import.write | Data in | A panel import made through the API failed. |
instrument.batch_dispatched | instruments.request | Clinical intelligence | A caseload questionnaire batch you started finished sending its requests. |
instrument.completed | instruments.read | Clinical intelligence | A patient completed a questionnaire this practice requested (after the crisis gate has run). |
medspa.adverse_event_reported | medspa.aftercare.read | Med spas | 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). |
member.account_claimed | members.claim_invite | Practice operations & reports | A patient the practice imported claimed their own account with the practice's invitation code. |
member.at_risk | fitness.roster.read | Fitness intelligence | A member newly entered churn level high in the daily scan. |
member.joined | members.read | Enrolment & messaging | A person linked to your organisation from the app's join screen. |
message.received | messages.read | Enrolment & messaging | A member wrote to your organisation in its in-app conversation. |
nutrition.caseload_flagged | nutrition.read | Nutrition (dietitians) | A member of the dietitian caseload carries a caseload flag: no food logged for 7 days, or 14-day calories outside 10 % of the target (Norvya conventions, not clinical guidelines). |
occupational.exam_due | occupational.read | Employers & occupational health | An exam of an active worker, not completed or cancelled, is due soon or past due (once per exam and due date). |
older_adults.falls_tier_raised | older_adults.read | Older adults | A patient's newest CDC STEADI self-check set a higher falls tier than their previous one, and the new tier is moderate or high. |
patient.admitted | transitions.notices.read | Care transitions | An admission matched to one of the practice's patients and visible to it. |
patient.attention_needed | clinical.panel.read | Clinical intelligence | A patient newly entered the top priority band in a completed current-data run. |
patient.discharged | transitions.notices.read | Care transitions | A discharge matched to one of the practice's patients and visible to it. |
patient.ed_visit | transitions.notices.read | Care transitions | An ED visit matched to one of the practice's patients and visible to it. |
pharmacy.discharge_episode_opened | pharmacy.patients.read | Pharmacy | A discharge follow-up episode opened. |
pharmacy.refill_due | pharmacy.patients.read | Pharmacy | A pack cycle of the pharmacy became due soon or overdue. |
population.export_completed | population.export | Population | A FHIR Bulk export you started finished (sent to that client only). |
public_health.case_report_response | public_health.case_reports.read | Public-health reporting | A Reportability Response was filed for one of the practice's eICRs. |
public_health.iis_ack_received | public_health.immunizations.read | Public-health reporting | A state immunization registry acknowledged a dose report of the practice. |
rehab.goal_met | rehab.read | Rehab (PT / OT / speech) | A functional goal the practice set was marked met (once per goal; only while the patient shares their medical history). |
roster.enrolment_summary | members.roster.read | Enrolment roster | A roster you uploaded and committed has new enrolment numbers (at most once a UTC day per roster, only when its counts changed). |
social_needs.referral_updated | social_needs.read | Social needs & programmes | A closed-loop community referral of the practice was created or changed status. |
social_needs.screening_completed | social_needs.read | Social needs & programmes | A patient submitted an AHC-HRSN screen. |
transition.episode_enrolled | transitions.read | Care transitions | A post-discharge episode opened at the hospital (staff desk, Partner API or an ADT A03). |
transition.red_flag | transitions.read | Care transitions | A patient's check-in tripped a new red flag. |
travel.inquiry_updated | travel.inquiries.read | Medical travel | An inquiry of the desk was created (in Norvya, through the API or from a hospital's request link) or changed status, including a new quote version and conversion. |
travel.invoice_issued | travel.billing.read | Medical travel | A monthly nursing invoice was issued. |
travel.journey_message_posted | travel.messages.read | Medical travel | Someone wrote in a journey's thread: the patient, their caregiver, desk staff in Norvya or another of your clients. |
travel.journey_updated | travel.journeys.read | Medical travel | A journey of the desk was opened, accepted or declined, moved a phase, or its nursing status changed. |
travel.leg_status_changed | travel.journeys.read | Medical travel | A flight or transfer was added, changed status or was cancelled. |
travel.nursing_visit_recorded | travel.nursing.read | Medical travel | A nurse recorded a visit note. |
worklist.item_triaged | clinical.panel.read | Clinical intelligence | A worklist finding was triaged by staff in the app or by another of the practice's clients (never echoed to the client that made the change). |
worklist.run_completed | clinical.panel.read | Clinical intelligence | A worklist run of the practice completed. |
Generated from the Partner API router and developers_catalog.py. The router docstring is the source of each description.