Make your first request
Mint an access token, check it with GET /v2/question_sets?limit=1, then list your most recent complete audit.
A live key works on your organization's records, so the calls below only read. To try writes, use a sandbox key, which works on sample data. The API is plain HTTPS and JSON, and the TypeScript and Python SDKs are optional.
Prerequisites
- API platform access in the dashboard, which an administrator of your organization turns on in Brellium. If you don't have it, see Get access to the dashboard.
- A live key. To create one on API keys, click Create key, choose Production and copy the Client ID and Client secret. The dashboard shows the secret only when you create the key. Rotate secret in the key's row menu issues a new one, also shown once.
Put both in your shell, keeping the secret out of your history:
export BRELLIUM_CLIENT_ID="your-client-id"
read -s BRELLIUM_CLIENT_SECRET && export BRELLIUM_CLIENT_SECRET
Mint a token
POST /v2/oauth/token mints an access token for the key, bound to your organization. Send the body with Content-Type: application/json. By default, curl's --data sends application/x-www-form-urlencoded, and a JSON body sent with that content type fails with an error that does not name the cause.
curl --request POST "https://api.brellium.com/v2/oauth/token" \
--header "Content-Type: application/json" \
--data @- <<EOF
{
"grant_type": "client_credentials",
"client_id": "$BRELLIUM_CLIENT_ID",
"client_secret": "$BRELLIUM_CLIENT_SECRET",
"audience": "https://api.brellium.dev"
}
EOF
{
"access_token": "eyJhbGciOi…redacted",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "documents:read documents:write audits:read ..."
}
The response has the token in access_token and its lifetime in expires_in, in seconds (about a day). POST /v2/oauth/token allows a burst of 20 requests per client_id, then one every 10 seconds; the 21st returns 429 RATE_LIMITED with Retry-After, so mint once and reuse the token.
Cache the token and reuse it until expires_at = now + expires_in - 300. Read expires_in from every response instead of hardcoding it. The TypeScript and Python samples below send the same request and compute when to re-mint:
// Keep credentials on the server; never in a browser or mobile app.
const response = await fetch("https://api.brellium.com/v2/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.BRELLIUM_CLIENT_ID,
client_secret: process.env.BRELLIUM_CLIENT_SECRET,
audience: "https://api.brellium.dev",
}),
});
if (!response.ok) {
throw new Error(`Token request failed: HTTP ${response.status}`);
}
const token = await response.json();
// Reuse the token until shortly before it expires; refresh 5 minutes early.
const expiresAt = Date.now() + (token.expires_in - 300) * 1000;
console.log(
token.token_type,
"token valid until",
new Date(expiresAt).toISOString(),
);
export {};
# Keep credentials on the server; never in a browser or mobile app.
import os
import time
import requests
response = requests.post(
"https://api.brellium.com/v2/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": os.environ["BRELLIUM_CLIENT_ID"],
"client_secret": os.environ["BRELLIUM_CLIENT_SECRET"],
"audience": "https://api.brellium.dev",
},
timeout=10,
)
response.raise_for_status()
token = response.json()
# Reuse the token until shortly before it expires; refresh 5 minutes early.
expires_at = time.time() + token["expires_in"] - 300
print(
token["token_type"],
"token valid until",
time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(expires_at)),
)
Store the token
Paste the access_token at the prompt so it stays out of your shell history:
read -s BRELLIUM_ACCESS_TOKEN && export BRELLIUM_ACCESS_TOKEN
Send it as Authorization: Bearer $BRELLIUM_ACCESS_TOKEN on every /v2 request until it expires.
Make your first request
List one question set. --write-out prints the HTTP status after the body; a 200 means your token works.
curl "https://api.brellium.com/v2/question_sets?limit=1" \
--header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN" \
--write-out "HTTP %{http_code}\n"
A question set holds the questions an audit is scored against. The response is a page with the newest question set in data:
{
"object": "list",
"data": [
{
"set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
"object": "question_set",
"set_name": "Progress Note Compliance",
"set_code": "PNC-1",
"type": "treatment_plan",
"thresholds": [
{ "flag_id": "5e4d3c2b-1a09-4f8e-9d7c-6b5a49382716", "score": 0.85 }
],
"questions": [
{
"question_id": "7d6c5b4a-3928-4170-a695-847362514f3e",
"object": "question",
"query": "Is the session note signed by the rendering provider?",
"question_type": "default",
"severity": "Critical",
"remediation": "Ask the rendering provider to sign the note.",
"affirmation": null
},
{
"question_id": "6c5b4a39-2817-4069-9584-736251403f2e",
"object": "question",
"query": "Does the note document the service start and end times?",
"question_type": "default",
"severity": "Normal",
"remediation": null,
"affirmation": "Service times are documented."
},
{
"question_id": "5b4a3928-1706-4f58-8473-625140392e1d",
"object": "question",
"query": "Which program does this note belong to?",
"question_type": "informational",
"severity": "None",
"remediation": null,
"affirmation": null
}
],
"created_at": "2026-09-01T12:00:00.000Z",
"updated_at": "2026-09-24T02:37:12.105Z"
}
],
"has_more": true
}
An organization with no question sets gets an empty page, and the status is still 200:
{
"object": "list",
"data": [],
"has_more": false
}
The samples below send the same request and print the HTTP status, the x-request-id header and the body:
// Keep credentials on the server; never in a browser or mobile app.
const response = await fetch("https://api.brellium.com/v2/question_sets?limit=1", {
headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
const body = await response.json();
console.log(
"HTTP",
response.status,
"request id",
response.headers.get("x-request-id"),
);
console.log(JSON.stringify(body, null, 2));
export {};
import BrelliumClient from "@brellium/api-client";
const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });
const { data, response } = await client.questionSets
.list({ limit: 1 })
.withResponse();
console.log(
"HTTP",
response.status,
"request id",
response.headers.get("x-request-id"),
);
console.log(JSON.stringify(data, null, 2));
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
raw = client.question_sets.with_raw_response.list(limit=1)
print("HTTP", raw.status_code, "request id", raw.headers.get("x-request-id"))
print(raw.parse().to_json())
# Keep credentials on the server; never in a browser or mobile app.
import json
import os
import requests
response = requests.get(
"https://api.brellium.com/v2/question_sets",
params={"limit": 1},
headers={"Authorization": f"Bearer {os.environ['BRELLIUM_ACCESS_TOKEN']}"},
timeout=10,
)
print(
"HTTP", response.status_code, "request id", response.headers.get("x-request-id")
)
print(json.dumps(response.json(), indent=2))
If the token request fails, check that you sent Content-Type: application/json. A 401 from POST /v2/oauth/token means the client id or secret was rejected. A 502 BAD_GATEWAY means the token endpoint is temporarily unavailable, so retry the mint with backoff (Authenticate with OAuth2 client credentials).
On a /v2 request, 401 UNAUTHORIZED means the access token is missing, invalid or expired; mint a new one and retry once. A 429 RATE_LIMITED means too many requests (Rate limits and errors).
Your first audit
An audit scores one or more documents against a question set. List the most recent audit whose status is complete:
curl "https://api.brellium.com/v2/audits?status=complete&limit=1" \
--header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const params = new URLSearchParams({ status: "complete", limit: "1" });
const res = await fetch(`https://api.brellium.com/v2/audits?${params}`, {
headers: { Authorization: `Bearer ${process.env.BRELLIUM_ACCESS_TOKEN}` },
});
const body = await res.json();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${JSON.stringify(body.error)}`);
console.log(body);
import BrelliumClient from "@brellium/api-client";
const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });
const audits = await client.audits.list({ status: "complete", limit: 1 });
console.log(audits);
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
audits = client.audits.list(status="complete", limit=1)
print(audits.to_json())
{
"object": "list",
"data": [
{
"audit_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"object": "audit",
"status": "complete",
"score": 0.6666666666666666,
"set_id": "3c2b1a09-8f7e-4d6c-b5a4-938271605f4e",
"flags": ["Failed", "Missing Signature"],
"created_at": "2026-09-24T02:34:54.489Z",
"updated_at": "2026-09-24T02:37:12.105Z",
"reviewed_at": null,
"display_info": [ /* 2 items: provider and date of service */ ],
"documents": ["0f8e7d6c-5b4a-4938-8271-605f4e3d2c1b"],
"people": [ /* 2 items: client and provider; see Understand an audit */ ],
"responses": [ /* 3 items, one per question; see Understand an audit */ ]
}
],
"has_more": true
}
An empty data array means no audit has completed yet. With a live key the response contains protected health information (PHI); a sandbox key returns sample audits.
An audit carries only the ids of its related records (set_id, documents[], people[].person_id); fetch each from its own endpoint. For every field, see Understand an audit.
Next
Authenticate with OAuth2 client credentials
Create a key, mint a token with it, cache the token, and handle 401, 429 and 502.
Upload documents
Create documents, upload each file, and follow them through processing, expiry and zips.
Keep audits in sync with updated_after
Backfill once, then poll updated_after windows that overlap and upsert by id.