Data in
Send your panel (a FHIR Bulk Data export, or a spreadsheet with a saved column mapping). You get a dry run first; patients are created only after commit, under a practice-of-record basis with a BAA on file.
- Who calls it
- Core for Concierge doctors, Independent practices, EMR vendors, Federal (via primes), State & local government, Hospitals and health systems; useful for Insurers (incl. Medicaid managed care), Practice management & billing, Specialty care
- Before production
- No gate of its own; your market's “Before production” applies.
- Sandbox
- Build against synthetic members first. What each sandbox holds
- Standards
- FHIR Bulk Data (panel import)
Markets that use this family
- Concierge doctors (core)
- Independent practices (core)
- EMR vendors (core)
- Federal (via primes) (core)
- State & local government (core)
- Insurers (incl. Medicaid managed care) (useful)
- Hospitals and health systems (core)
- Practice management & billing (useful)
- Specialty care (useful)
Scopes
| Scope | Allows |
|---|---|
panel.import.write | Stage, review, commit and discard practice panel imports (FHIR Bulk Data, or a spreadsheet with a saved mapping profile; medical practices with a BAA on file only). |
Endpoints
Stage a panel import and get its dry run
Parses and stages the file (held encrypted; nothing reaches a live table) and returns the dry run. Identity is the MRN only — never a name or birth date match. A spreadsheet is accepted ONLY when every sheet matches a mapping profile saved by your practice's admin in Norvya (no interactive mapping over the API): 422 panel_import_mapping_profile_required otherwise. Medical practice with a BAA on file only (409 not_a_medical_practice / panel_import_baa_required).
Request body
Content type: multipart/form-data (required)
A FHIR Bulk Data export (one or more .ndjson files, or a .zip of them), a FHIR Bundle (.json), or a spreadsheet (.xlsx / .csv) your practice has a saved mapping profile for.
FHIR: the Patient identifier system that holds your MRN (default: the identifier typed MR).
Errors specific to this call
422panel_import_mapping_profile_required409not_a_medical_practiceAll 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/panel/imports" \
-H "Authorization: Bearer $TOKEN" \
-F files=@your_file \
-F identifier_system="$IDENTIFIER_SYSTEM"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
with open("your_file", "rb") as fh:
resp = requests.post(
BASE + "/v1/panel/imports",
headers={"Authorization": f"Bearer {TOKEN}"},
files={"files": fh},
data={"identifier_system": os.environ["IDENTIFIER_SYSTEM"]},
timeout=30,
)
resp.raise_for_status()
print(resp.json())// Node.js 18+ (ES module). Keep production credentials on your server.
import { readFile } from "node:fs/promises";
const BASE = "https://norvya.fly.dev";
const url = new URL(`${BASE}/v1/panel/imports`);
const form = new FormData();
form.append("files", new Blob([await readFile("your_file")]), "your_file");
form.append("identifier_system", process.env.IDENTIFIER_SYSTEM);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
},
body: form,
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"id": 0,
"status": "string",
"file_format": "string",
"filename": "string",
"file_sha256": "string",
"file_bytes": 0,
"created_at": "string",
"committed_at": "string",
"discarded_at": "string",
"error_code": "string",
"patients_in_file": 0,
"records": 0,
"rejected": 0,
"progress": {},
"can_commit": false,
"blocking": [
"string"
],
"summary": {},
"result": {},
"data_quality": {},
"changes": {}
}The response is the shape from the schema, not live data.
One panel import: dry run, progress, result
Imports made through the API by your practice only (404 panel_import_not_found otherwise).
Parameters
Errors specific to this call
404panel_import_not_foundAll responses
200Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/panel/imports/$IMPORT_ID" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
import_id = os.environ["IMPORT_ID"]
resp = requests.get(
f"{BASE}/v1/panel/imports/{import_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 import_id = process.env.IMPORT_ID;
const url = new URL(`${BASE}/v1/panel/imports/${encodeURIComponent(import_id)}`);
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());{
"id": 0,
"status": "string",
"file_format": "string",
"filename": "string",
"file_sha256": "string",
"file_bytes": 0,
"created_at": "string",
"committed_at": "string",
"discarded_at": "string",
"error_code": "string",
"patients_in_file": 0,
"records": 0,
"rejected": 0,
"progress": {},
"can_commit": false,
"blocking": [
"string"
],
"summary": {},
"result": {},
"data_quality": {},
"changes": {}
}The response is the shape from the schema, not live data.
Try it in your sandbox
Commit a staged import
Writes the import in the background (poll GET, or subscribe to import.completed / import.failed). New MRNs become managed, login-disabled patient records under the practice-of-record basis. Refused (409) with the dry run's first blocking code (e.g. panel_import_date_format_unconfirmed, panel_import_nothing_to_import), panel_import_baa_required when no BAA is on file NOW, panel_import_not_staged, panel_import_commit_in_progress. The BAA is checked again by the worker before the first write.
Parameters
Request body
Content type: application/json
All responses
202Successful Response401Unauthorized403Forbidden404Not Found409Conflict422Unprocessable Entity429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/panel/imports/$IMPORT_ID/commit" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"run_worklist": false,
"create_possible_duplicates": false
}'import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
import_id = os.environ["IMPORT_ID"]
resp = requests.post(
f"{BASE}/v1/panel/imports/{import_id}/commit",
headers={"Authorization": f"Bearer {TOKEN}"},
json={
"run_worklist": False,
"create_possible_duplicates": False,
},
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 import_id = process.env.IMPORT_ID;
const url = new URL(`${BASE}/v1/panel/imports/${encodeURIComponent(import_id)}/commit`);
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"run_worklist": false,
"create_possible_duplicates": false
}),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"id": 0,
"status": "string",
"file_format": "string",
"filename": "string",
"file_sha256": "string",
"file_bytes": 0,
"created_at": "string",
"committed_at": "string",
"discarded_at": "string",
"error_code": "string",
"patients_in_file": 0,
"records": 0,
"rejected": 0,
"progress": {},
"can_commit": false,
"blocking": [
"string"
],
"summary": {},
"result": {},
"data_quality": {},
"changes": {}
}The response is the shape from the schema, not live data.
Try it in your sandbox
Discard a staged import
Deletes the held file; the manifest (counts, reasons, hash) stays.
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/panel/imports/$IMPORT_ID/discard" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
import_id = os.environ["IMPORT_ID"]
resp = requests.post(
f"{BASE}/v1/panel/imports/{import_id}/discard",
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 import_id = process.env.IMPORT_ID;
const url = new URL(`${BASE}/v1/panel/imports/${encodeURIComponent(import_id)}/discard`);
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());{
"id": 0,
"status": "string",
"file_format": "string",
"filename": "string",
"file_sha256": "string",
"file_bytes": 0,
"created_at": "string",
"committed_at": "string",
"discarded_at": "string",
"error_code": "string",
"patients_in_file": 0,
"records": 0,
"rejected": 0,
"progress": {},
"can_commit": false,
"blocking": [
"string"
],
"summary": {},
"result": {},
"data_quality": {},
"changes": {}
}The response is the shape from the schema, not live data.
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 |
|---|---|---|
import.completed | panel.import.write | A panel import made through the API committed. |
import.failed | panel.import.write | A panel import made through the API failed. |
Generated from the Partner API router and developers_catalog.py. The router docstring is the source of each description.