Clients and SDKs
Brellium has official TypeScript and Python SDKs. The API itself is plain HTTPS and JSON at https://api.brellium.com. You can also call it with curl, generate a typed client from the OpenAPI document, or start from a hand-written TypeScript or Python client of under a hundred lines.
Each /v2 request needs an Authorization: Bearer header. Send request bodies as snake_case JSON with Content-Type: application/json; any other content type, or none, returns 400 INVALID_CONTENT_TYPE. An error from a /v2 endpoint is one JSON envelope that carries a request_id. A live key works on your organization's records, including when you use it with the Test Request button in the API reference; a sandbox key works on sample data.
With an access token in $BRELLIUM_ACCESS_TOKEN (Authenticate with OAuth2 client credentials), this request fetches one audit by id:
curl "https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d" \
--header "Authorization: Bearer $BRELLIUM_ACCESS_TOKEN"
const res = await fetch(
"https://api.brellium.com/v2/audits/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
{ 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 audit = await client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
console.log(audit);
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
audit = client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d")
print(audit.to_json())
With a sandbox key, the response is the sample audit (Understand an audit). With a live key, the sample id returns 404 NOT_FOUND, so replace it with an id from your own GET /v2/audits list.
TypeScript SDK
Install the @brellium/api-client package:
npm install @brellium/api-client
Pass an access token as oAuth2, either a string or a function that returns one, and call a resource method:
import BrelliumClient from "@brellium/api-client";
const client = new BrelliumClient({ oAuth2: process.env.BRELLIUM_ACCESS_TOKEN });
const audit = await client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d");
console.log(audit.status, audit.score, audit.flags);
The SDK does not mint tokens. Mint one at POST /v2/oauth/token and reuse it until it expires (Mint one access token and reuse it).
The SDK retries a connection error and a 408, 409, 429 or 5xx response twice by default, and honors Retry-After. Set maxRetries on the client or on one request to change that. The retries apply to every method, POST included, so pass maxRetries: 0 on a write that is not safe to repeat (Handle rate limits and retries). A non-success response throws an APIError with status, headers and the parsed error body in error.
Python SDK
Install the brellium-api-client package, which needs Python 3.9 or later:
pip install brellium-api-client
Pass an access token as o_auth2, or leave it out and the client reads BRELLIUM_ACCESS_TOKEN from the environment. Then call a resource method:
import os
from brellium_api_client import BrelliumClient
client = BrelliumClient(o_auth2=os.environ["BRELLIUM_ACCESS_TOKEN"])
audit = client.audits.retrieve("a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d")
print(audit.status, audit.score, audit.flags)
AsyncBrelliumClient has the same methods for asyncio: audit = await client.audits.retrieve(...).
The SDK does not mint tokens. Mint one at POST /v2/oauth/token and reuse it until it expires (Mint one access token and reuse it).
The SDK retries a connection error and a 408, 409, 429 or 5xx response twice by default, and honors Retry-After. Set max_retries on the client, or for one call through client.with_options(), to change that. The retries apply to every method, POST included, so turn them off on a write that is not safe to repeat (Handle rate limits and retries). A non-success response raises an APIStatusError, or a subclass such as NotFoundError or RateLimitError, with status_code, response and the parsed error body in body.
For other languages, generate a client or start from the hand-written one below.
Other ways to call the API
Without an SDK, you can call the API in three ways:
| Approach | Use it for | What you still write |
|---|---|---|
| curl | A one-off check, a shell or CI script, a request reproduced for Brellium support, or the calls in Make your first request | Everything: the token in a shell variable, the Retry-After wait, and the last id copied into starting_after for the next page |
| Generated client | Autocomplete and compile-time checks across many endpoints, or a stack that already generates from OpenAPI | Token caching, 429 retries and the pagination loop, which a generator does not write |
Hand-written fetch or requests |
A few endpoints, a project that takes no new dependencies, or a place where code generation is awkward | Under a hundred lines, as in the skeleton below |
Every operation page of the reference, for example Retrieve an audit, shows its request in a language picker (curl by default) next to a Test Request button.
Generate a typed client
https://api.brellium.com/v2/openapi.json is the OpenAPI 3.1 document of the running API, and the reference is built from it. Any OpenAPI 3.1 generator accepts it. For the steps with openapi-typescript and openapi-fetch, or with openapi-generator for Python, see Generate a typed client from the OpenAPI document. A generated client still needs the token cache, the 429 wait and the pagination loop from the hand-written client below.
| Generator | Output |
|---|---|
openapi-typescript 7 with openapi-fetch |
TypeScript types only (brellium.d.ts, operations["getAudit"]); openapi-fetch checks each call by literal path and method |
openapi-generator 7.19, -g typescript-fetch |
A TypeScript package with one class per tag, AuditsApi.getAudit and AuditsApi.updateAuditFlags |
openapi-generator 7.19, -g python |
A Python package with one class per tag and a pydantic model per schema, AuditsApi.get_audit |
A minimal hand-written client
The skeleton, in TypeScript and Python, has the parts an SDK would have: a token cache, a request helper and a list iterator. The token cache mints one token and reuses it until expires_at = now + expires_in - 300 (Authenticate with OAuth2 client credentials). The request helper sends the bearer header, waits Retry-After seconds on a 429, re-mints once on a 401, and raises errors with the envelope's code, message and request_id (Rate limits and errors). The list iterator pages with starting_after until has_more is false.
Both clients read the client id and client secret from environment variables. The Python client needs Python 3.10 or later and one package, requests; the TypeScript client uses no package.
// Keep credentials on the server; never in a browser or mobile app.
const BASE_URL = "https://api.brellium.com";
const CLIENT_ID = process.env.BRELLIUM_CLIENT_ID;
const CLIENT_SECRET = process.env.BRELLIUM_CLIENT_SECRET;
if (!CLIENT_ID || !CLIENT_SECRET) {
throw new Error("Set BRELLIUM_CLIENT_ID and BRELLIUM_CLIENT_SECRET");
}
type TokenResponse = { access_token: string; expires_in: number };
type Page<T> = {
object: "list";
data: T[];
has_more: boolean;
total_count?: number;
};
type ApiError = {
error: { type: string; code: string; message: string; request_id?: string };
};
type Audit = {
audit_id: string;
status: string;
score: number | null;
set_id: string;
documents: string[];
};
let cached: { token: string; expiresAt: number } | undefined;
// 1. Mint once, cache until expires_in minus 300 s.
async function accessToken(): Promise<string> {
if (cached && Date.now() < cached.expiresAt) return cached.token;
const res = await fetch(`${BASE_URL}/v2/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
audience: "https://api.brellium.dev",
}),
});
if (!res.ok) throw new Error(`POST /v2/oauth/token failed: ${res.status}`);
const body = (await res.json()) as TokenResponse;
cached = {
token: body.access_token,
expiresAt: Date.now() + (body.expires_in - 300) * 1000,
};
return cached.token;
}
// 2. One request helper: bearer header, JSON body, wait Retry-After on 429, re-mint
// once on 401.
async function request<T>(
method: string,
path: string,
body?: unknown,
): Promise<T> {
let reminted = false;
for (let attempt = 0; ; attempt++) {
const headers: Record<string, string> = {
Authorization: `Bearer ${await accessToken()}`,
};
if (body !== undefined) headers["Content-Type"] = "application/json";
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
if (res.status === 429 && attempt < 5) {
const seconds = Number(res.headers.get("Retry-After") ?? "1");
await new Promise((resolve) =>
setTimeout(resolve, Math.max(seconds, 1) * 1000),
);
continue;
}
if (res.status === 401 && !reminted) {
cached = undefined;
reminted = true;
continue;
}
if (res.status === 204) return undefined as unknown as T;
const json = await res.json();
if (!res.ok) {
const { error } = json as ApiError;
throw new Error(
`${method} ${path}: ${res.status} ${error.code} ${error.message} ` +
`(request_id ${error.request_id})`,
);
}
return json as T;
}
}
// 3. Page a list: limit=100, starting_after = the last item's id field, stop when
// has_more is false.
// idField names the resource's id: "audit_id", "user_id", "person_id" and so on.
async function* list<T>(
path: string,
idField: keyof T,
query: Record<string, string> = {},
): AsyncGenerator<T> {
let startingAfter: string | undefined;
do {
const params = new URLSearchParams({ ...query, limit: "100" });
if (startingAfter) params.set("starting_after", startingAfter);
const page = await request<Page<T>>("GET", `${path}?${params.toString()}`);
yield* page.data;
const last = page.data[page.data.length - 1];
startingAfter = page.has_more && last ? String(last[idField]) : undefined;
} while (startingAfter);
}
async function main(): Promise<void> {
// Take the first complete audit from the list, then fetch it by id.
for await (const item of list<Audit>("/v2/audits", "audit_id", {
status: "complete",
})) {
const audit = await request<Audit>("GET", `/v2/audits/${item.audit_id}`);
console.log(
audit.audit_id,
audit.status,
audit.score,
audit.set_id,
audit.documents.length,
);
break;
}
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
# Keep credentials on the server; never in a browser or mobile app.
import os
import time
from collections.abc import Iterator
from typing import Any
import requests
BASE_URL = "https://api.brellium.com"
class Brellium:
def __init__(self, client_id: str, client_secret: str) -> None:
self._client_id = client_id
self._client_secret = client_secret
self._session = requests.Session()
self._token: str | None = None
self._expires_at = 0.0
# 1. Mint once, cache until expires_in minus 300 s.
def _access_token(self) -> str:
if self._token and time.monotonic() < self._expires_at:
return self._token
r = self._session.post(
f"{BASE_URL}/v2/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": self._client_id,
"client_secret": self._client_secret,
"audience": "https://api.brellium.dev",
},
timeout=30,
)
r.raise_for_status()
body = r.json()
self._token = body["access_token"]
self._expires_at = time.monotonic() + body["expires_in"] - 300
return self._token
# 2. One request helper: bearer header, JSON body, wait Retry-After on 429,
# re-mint once on 401.
def request(
self,
method: str,
path: str,
body: Any = None,
params: dict[str, Any] | None = None,
) -> Any:
reminted = False
attempt = 0
while True:
r = self._session.request(
method,
f"{BASE_URL}{path}",
params=params,
json=body,
headers={"Authorization": f"Bearer {self._access_token()}"},
timeout=30,
)
if r.status_code == 429 and attempt < 5:
attempt += 1
time.sleep(max(float(r.headers.get("Retry-After", "1")), 1.0))
continue
if r.status_code == 401 and not reminted:
self._token, reminted = None, True
continue
if r.status_code == 204:
return None
payload = r.json()
if not r.ok:
err = payload["error"]
raise RuntimeError(
f"{method} {path}: {r.status_code} {err['code']} "
f"{err['message']} "
f"(request_id {err.get('request_id')})"
)
return payload
# 3. Page a list: limit=100, starting_after = the last item's id field, stop
# when has_more is false.
# id_field names the resource's id: "audit_id", "user_id", "person_id" and so
# on.
def iter_list(
self, path: str, id_field: str, **filters: str
) -> Iterator[dict[str, Any]]:
params: dict[str, Any] = {"limit": 100, **filters}
while True:
page = self.request("GET", path, params=params)
yield from page["data"]
if not page["has_more"] or not page["data"]:
return
params["starting_after"] = page["data"][-1][id_field]
if __name__ == "__main__":
api = Brellium(
os.environ["BRELLIUM_CLIENT_ID"], os.environ["BRELLIUM_CLIENT_SECRET"]
)
# Take the first complete audit from the list, then fetch it by id.
for item in api.iter_list("/v2/audits", "audit_id", status="complete"):
audit = api.request("GET", f"/v2/audits/{item['audit_id']}")
print(
audit["audit_id"],
audit["status"],
audit["score"],
audit["set_id"],
len(audit["documents"]),
)
break
To call the sandbox, use the client id and client secret of a sandbox key and set audience to https://api.brellium.dev/sandbox. Nothing else in the skeleton changes.
The skeleton leaves out backoff with jitter beyond Retry-After (Handle rate limits and retries) and a token store shared across your processes (Mint one access token and reuse it). Its token request throws on any error from POST /v2/oauth/token, including 429 RATE_LIMITED and 502 BAD_GATEWAY, and does not retry (Authenticate with OAuth2 client credentials). The skeleton also leaves out request timeouts tuned to your runtime (the Python client passes a fixed timeout=30; the TypeScript client sets none).
Next
Generate a typed client from the OpenAPI document
Generate a TypeScript or Python client from openapi.json and keep it current.
Mint one access token and reuse it
Mint a token once, cache it until expires_in, re-mint on 401, wait on 429.
Handle rate limits and retries
Read the RateLimit headers, back off on 429, retry only safe calls, and tell a 403 from a 429.
Page through and filter lists
Cursor pagination, batch get by id, and each resource's filters.