Platform
Tokens, your member list, and your own usage. Every integration starts here. EMR vendors use one credential for every practice that approves them in Norvya: GET /v1/practices lists those practices, and a token is requested per practice.
- 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
Scopes
| Scope | Allows |
|---|---|
members.read | List the organisation's linked members as opaque member references. |
vendor.practices.read | EMR vendors: list the practices that have approved your integration (a vendor-level token, requested without a practice; no patient data). |
Endpoints
Exchange client credentials for a bearer token
OAuth2 client_credentials (RFC 6749 §4.4). Credentials in the form body or HTTP Basic. The token lives 15 minutes, carries the client's clinic and scopes, and stops working at once if the client is revoked.
EMR vendors (client ids starting pvc_) serve many practices with one credential: with practice=<practice_ref> the token is bound to that practice and carries the scopes you request that the practice approved (all of them when you request none); revoking the approval stops it at once. Without practice you get a vendor-level token for GET /v1/practices only. An unknown, revoked or someone else's practice_ref is invalid_grant.
Request body
Content type: application/x-www-form-urlencoded (required)
Must be client_credentials.
Optional space-separated subset of the client's scopes.
EMR vendors only: the practice_ref of a practice that approved you (GET /v1/practices). Omit for a vendor-level token (scope vendor.practices.read).
All responses
200Successful Response400Bad Request401Unauthorized422Validation ErrorCodes every call can return are in Errors & limits.
curl -s -X POST "https://norvya.fly.dev/v1/oauth/token" \
-d grant_type=client_credentials \
-d client_id=$CLIENT_ID \
-d client_secret=$CLIENT_SECRET \
-d scope=$SCOPEimport os
import requests
BASE = "https://norvya.fly.dev"
resp = requests.post(
BASE + "/v1/oauth/token",
data={"grant_type": "client_credentials", "client_id": os.environ["CLIENT_ID"], "client_secret": os.environ["CLIENT_SECRET"], "scope": os.environ["SCOPE"]},
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/oauth/token`);
const res = await fetch(url, {
method: "POST",
body: new URLSearchParams({ "grant_type": "client_credentials", "client_id": process.env.CLIENT_ID, "client_secret": process.env.CLIENT_SECRET, "scope": process.env.SCOPE }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
console.log(await res.json());{
"access_token": "string",
"token_type": "string",
"expires_in": 0,
"scope": "string"
}The response is the shape from the schema, not live data.
List your organisation's linked members
Active members linked to your organisation, as opaque member_refs (plus your own id when the member is linked through a system sync). Nothing else about the member is returned here.
All responses
200Successful Response401Unauthorized403Forbidden404Not Found429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/members" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/members",
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/members`);
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());{
"count": 0,
"members": [
{
"member_ref": "string",
"external_id": "string",
"linked_at": "string"
}
]
}The response is the shape from the schema, not live data.
Try it in your sandbox
Your client's call counts, last 30 days
Calls and errors per endpoint per UTC day for the calling client, for usage-based pricing. Counts every authenticated call, including refusals (counted in errors).
All responses
200Successful Response401Unauthorized403Forbidden404Not Found429Too Many Requests503Service UnavailableCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/usage" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/usage",
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/usage`);
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());{
"client_id": "string",
"from": "string",
"to": "string",
"total_calls": 0,
"by_endpoint": {},
"days": [
{
"day": "string",
"endpoint": "string",
"calls": 0,
"errors": 0
}
],
"environment": "string",
"billable_calls": 0
}The response is the shape from the schema, not live data.
Try it in your sandbox
EMR vendors: the practices that approved your integration
Every practice with an ACTIVE approval of your integration: its practice_ref (ask POST /v1/oauth/token for practice=<practice_ref>), its name, the scopes it approved and when. Nothing else about the practice and nothing about its patients. Needs a vendor-level token (requested without practice); a practice-bound token gets 403 insufficient_scope.
Errors specific to this call
403insufficient_scopeAll responses
200Successful Response401Unauthorized403Forbidden429Too Many RequestsCodes every call can return are in Errors & limits.
curl -s "https://norvya.fly.dev/v1/practices" \
-H "Authorization: Bearer $TOKEN"import os
import requests
BASE = "https://norvya.fly.dev"
TOKEN = os.environ["TOKEN"]
resp = requests.get(
BASE + "/v1/practices",
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/practices`);
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());{
"vendor_id": "pv_Hk2m9QzR4tYv",
"count": 0,
"practices": [
{
"practice_ref": "pr_x2Wq9vT0bLr3sK8mN1aZ4g",
"practice_name": "Riverside Family Medicine",
"scopes": [
"clinical.brief.read",
"clinical.patient.match"
],
"granted_at": "2026-10-15T14:02:11Z"
}
]
}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.