Skip to content

Call the API

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.

Terminal window
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 bash
set -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 1
fi
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 1
fi
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 json
import os
import time
from urllib.parse import quote, urlencode
from 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));
  • Keep the token in a server-side process. Never run these calls from a public browser bundle.
  • Re-run discovery instead of assuming that hall ownership or sensor registration never changes.
  • Keep compensate=0 when the integration needs the original device matrix.
  • Treat an empty frame window as a valid 200 response and inspect missing.
  • Use bounded windows and deliberate polling; the archive endpoint is not a live stream.

Authentication

Protect, rotate, and revoke an Organization API Token.

Raw frame contract

Review the response fields, limits, and matrix format.