Authentication
Protect, rotate, and revoke an Organization API Token.
Each example discovers an organization and hall, selects a registered sensor, and reads up to ten uncompensated raw frames from the last five minutes. The token stays in an environment variable and is never printed.
Store the once-only token outside the project, restrict the file, then load it
into the current process. Do not commit either the token or an .env file that
contains it.
install -d -m 700 "$HOME/.config/butlr"chmod 600 "$HOME/.config/butlr/organization-api.token"export BUTLR_DC_API_TOKEN="$(<"$HOME/.config/butlr/organization-api.token")"export BUTLR_DC_API_BASE='https://api.datacenter.butlr.io'Requires Bash or Zsh, curl, and jq.
#!/usr/bin/env bashset -euo pipefail
: "${BUTLR_DC_API_TOKEN:?Set BUTLR_DC_API_TOKEN first}"BUTLR_DC_API_BASE="${BUTLR_DC_API_BASE:-https://api.datacenter.butlr.io}"
graphql() { curl --fail-with-body --silent --show-error \ "$BUTLR_DC_API_BASE/graphql" \ --header "Authorization: Bearer $BUTLR_DC_API_TOKEN" \ --header 'Content-Type: application/json' \ --data "$1"}
inventory=$(graphql '{"query":"query OrganizationInventory { orgs { id name } dataHalls { id name } }"}')if jq -e '.errors | length > 0' >/dev/null 2>&1 <<<"$inventory"; then jq '.errors' <<<"$inventory" >&2 exit 1fi
hall_id="${BUTLR_DC_HALL_ID:-$(jq -er '.data.dataHalls[0].id' <<<"$inventory")}"sensor_request=$(jq -cn --arg hallId "$hall_id" '{ query: "query HallSensors($hallId: ID!) { sensors(hallId: $hallId) { id kind mac serial model status } }", variables: {hallId: $hallId}}')sensors=$(graphql "$sensor_request")if jq -e '.errors | length > 0' >/dev/null 2>&1 <<<"$sensors"; then jq '.errors' <<<"$sensors" >&2 exit 1fi
sensor_key=$(jq -er 'first( .data.sensors[] | if .kind == "wireless_mac" then .mac elif .kind == "wired_serial" then .serial else empty end | select(. != null and . != ""))' <<<"$sensors")
to_ms="$(( $(date +%s) * 1000 ))"from_ms="$(( to_ms - 300000 ))"
curl --fail-with-body --silent --show-error --get \ "$BUTLR_DC_API_BASE/v1/sensors/$sensor_key/frames" \ --header "Authorization: Bearer $BUTLR_DC_API_TOKEN" \ --data-urlencode "from=$from_ms" \ --data-urlencode "to=$to_ms" \ --data-urlencode 'limit=10' \ --data-urlencode 'compensate=0' | jq '{key, kind, tier, from, to, count, total, sampled, missing, compensated}'Remove the final jq projection when your application is ready to process the
full frames[].data matrices.
Uses only the Python 3 standard library.
import jsonimport osimport timefrom urllib.parse import quote, urlencodefrom urllib.request import Request, urlopen
BASE_URL = os.getenv("BUTLR_DC_API_BASE", "https://api.datacenter.butlr.io").rstrip("/")TOKEN = os.environ["BUTLR_DC_API_TOKEN"].strip()
def request_json(url, *, payload=None): body = None if payload is None else json.dumps(payload).encode() request = Request(url, data=body) request.add_header("Authorization", f"Bearer {TOKEN}") if body is not None: request.add_header("Content-Type", "application/json") with urlopen(request, timeout=30) as response: return json.load(response)
def graphql(query, variables=None): result = request_json( f"{BASE_URL}/graphql", payload={"query": query, "variables": variables or {}}, ) if result.get("errors"): raise RuntimeError(json.dumps(result["errors"])) return result["data"]
inventory = graphql( "query OrganizationInventory { orgs { id name } dataHalls { id name } }")hall_id = os.getenv("BUTLR_DC_HALL_ID") or inventory["dataHalls"][0]["id"]
sensors = graphql( """query HallSensors($hallId: ID!) { sensors(hallId: $hallId) { id kind mac serial model status } }""", {"hallId": hall_id},)["sensors"]
sensor = next( sensor for sensor in sensors if (sensor.get("mac") if sensor["kind"] == "wireless_mac" else sensor.get("serial") if sensor["kind"] == "wired_serial" else None))sensor_key = sensor["mac"] if sensor["kind"] == "wireless_mac" else sensor["serial"]
to_ms = int(time.time() * 1000)query = urlencode({"from": to_ms - 300_000, "to": to_ms, "limit": 10, "compensate": 0})frames = request_json( f"{BASE_URL}/v1/sensors/{quote(sensor_key, safe='')}/frames?{query}")
first_matrix = frames["frames"][0]["data"] if frames["frames"] else []print(json.dumps({ "organization": inventory["orgs"][0]["name"], "hall_id": hall_id, "sensor_key": sensor_key, "tier": frames["tier"], "count": frames["count"], "missing": frames["missing"], "matrix_rows": len(first_matrix), "matrix_columns": len(first_matrix[0]) if first_matrix else 0,}, indent=2))Requires Node.js 18 or later for the built-in fetch implementation.
const baseUrl = (process.env.BUTLR_DC_API_BASE ?? "https://api.datacenter.butlr.io") .replace(/\/$/, "");const token = process.env.BUTLR_DC_API_TOKEN?.trim();if (!token) throw new Error("Set BUTLR_DC_API_TOKEN first");
async function requestJson(url, init = {}) { const response = await fetch(url, { ...init, headers: { Authorization: `Bearer ${token}`, ...(init.body ? { "Content-Type": "application/json" } : {}), ...init.headers, }, }); const body = await response.json(); if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(body)}`); return body;}
async function graphql(query, variables = {}) { const result = await requestJson(`${baseUrl}/graphql`, { method: "POST", body: JSON.stringify({ query, variables }), }); if (result.errors?.length) throw new Error(JSON.stringify(result.errors)); return result.data;}
const inventory = await graphql( "query OrganizationInventory { orgs { id name } dataHalls { id name } }",);const hallId = process.env.BUTLR_DC_HALL_ID ?? inventory.dataHalls[0].id;const { sensors } = await graphql( `query HallSensors($hallId: ID!) { sensors(hallId: $hallId) { id kind mac serial model status } }`, { hallId },);
const sensor = sensors.find((candidate) => candidate.kind === "wireless_mac" ? candidate.mac : candidate.kind === "wired_serial" ? candidate.serial : false,);if (!sensor) throw new Error("No supported sensor was returned for this hall");const sensorKey = sensor.kind === "wireless_mac" ? sensor.mac : sensor.serial;
const to = Date.now();const query = new URLSearchParams({ from: String(to - 300_000), to: String(to), limit: "10", compensate: "0",});const frames = await requestJson( `${baseUrl}/v1/sensors/${encodeURIComponent(sensorKey)}/frames?${query}`,);
const firstMatrix = frames.frames[0]?.data ?? [];console.log(JSON.stringify({ organization: inventory.orgs[0].name, hallId, sensorKey, tier: frames.tier, count: frames.count, missing: frames.missing, matrixRows: firstMatrix.length, matrixColumns: firstMatrix[0]?.length ?? 0,}, null, 2));compensate=0 when the integration needs the original device matrix.200 response and inspect missing.Authentication
Protect, rotate, and revoke an Organization API Token.
Raw frame contract
Review the response fields, limits, and matrix format.