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