Skip to content

Barbara API SDK for Python

Official Python SDK for the Barbara Edge AI platform API.

Typed, synchronous and asynchronous clients for managing nodes, clusters, applications, models, and related resources.

Source{ .md-button } PyPI{ .md-button }

What Barbara manages

Before using the SDK it helps to understand the domain it operates on, because the resource tree (client.nodes, client.clusters, client.applications, ...) mirrors it directly.

Barbara is a management platform for Edge AI: instead of running inference or data processing in a central cloud, the workload runs physically close to where the data is produced: a factory floor, a retail store, a vehicle, a piece of industrial equipment. Barbara's role is to be the control plane for the machines running there.

A few terms recur throughout the API and this SDK. See also the Barbara Academy platform documentation for the concepts as they appear in the Panel UI.

Node. A physical or virtual machine running the Barbara agent: anything from a small industrial gateway to a server-grade box. Operators in the Barbara Panel usually refer to it by a human-readable name, the Barbara ID (for example factory-line-3-cam01). A node reports telemetry (status, resource usage, Barbara Core version) and is the unit onto which applications and models get deployed. See Node lifecycle in Academy.

Cluster. A group of nodes managed and deployed to as a unit. Rather than pushing an application to ten nodes one at a time, a cluster lets you push it once and have it land on every member. Clusters have their own secrets and configuration, independent of any single node's. See High availability in Academy.

Application. A packaged, versioned piece of software (typically a Docker-based service) that can be installed on a node or across a cluster. Barbara distinguishes between applications you author yourself (Docker applications) and applications published in Barbara's marketplace (Marketplace applications); the SDK reflects this with separate create_docker_workload / create_marketplace_workload methods. See App Library in Academy.

Workload. An application actually running — either on one specific node, or (the cluster-level equivalent) across every node in a cluster. Barbara's product docs use this one term for both scopes; the SDK mirrors that with client.nodes.workloads and client.clusters.workloads. Both expose lifecycle operations (start, stop, logs) once deployed.

Model. A machine learning model artifact (for example, an ONNX file), versioned the same way applications are, and deployed to nodes to run inference at the edge. See Models in Academy.

Group. A named collection of nodes used for organizing and filtering, independent of clusters (a group does not imply a shared deployment).

Why this matters for the SDK

Every resource in BarbaraClient corresponds to one of these concepts, and the nesting mirrors reality: node-scoped workloads live under nodes (client.nodes.workloads), and cluster-scoped workloads live under clusters (client.clusters.workloads) — same vocabulary, same operations, different scope.

Requirements

  • Python 3.9 or later

Installation

pip install barbara-api-sdk

Authentication

The Barbara API uses the OAuth2 password grant: the SDK exchanges a set of credentials for a short-lived bearer token and refreshes it automatically. You need four values, referred to throughout Barbara's documentation as the Barbara API Credentials:

Credential Description
BBR_API_USERNAME Your Barbara Panel username
BBR_API_PASSWORD Your Barbara Panel password
BBR_API_CLIENT_ID OAuth2 client ID, provided by Barbara
BBR_API_CLIENT_SECRET OAuth2 client secret, provided by Barbara

Where these come from

BBR_API_USERNAME and BBR_API_PASSWORD are the same credentials used to log into the Barbara Panel. Create a free account at onboarding.barbara.tech if you don't have one. BBR_API_CLIENT_ID and BBR_API_CLIENT_SECRET are issued separately by Barbara support; they identify the OAuth2 client, not a personal account.

The simplest way to authenticate is to export all four as environment variables and let the client pick them up:

export BBR_API_USERNAME="..."
export BBR_API_PASSWORD="..."
export BBR_API_CLIENT_ID="..."
export BBR_API_CLIENT_SECRET="..."
from barbara import BarbaraClient

client = BarbaraClient.from_env()

Three more environment variables control where the client connects, and rarely need to change:

Variable Description Default
BBR_API_URL Barbara API base URL https://prod.bap.barbara.tech
BBR_AUTH_URL Barbara auth server base URL https://prod.auth.barbara.tech/auth
BBR_REALM Authentication realm bbr_prod

If you'd rather not rely on environment variables (for example, when credentials come from a secrets manager), construct a BarbaraConfig explicitly:

from barbara import BarbaraClient, BarbaraConfig

config = BarbaraConfig(
    client_id="...",
    client_secret="...",
    username="...",
    password="...",
)
client = BarbaraClient(config)

Token lifetime is short

Access tokens issued by the Barbara auth server expire after roughly one hour. You do not need to handle this yourself: every request the SDK makes checks token freshness first, and a request that still comes back 401 is retried exactly once with a freshly fetched token. If it fails a second time, the original error is raised: at that point the credentials themselves are the likely problem, not an expired token.

Quick start

from barbara import BarbaraClient

with BarbaraClient.from_env() as client:
    for node in client.nodes.list():
        print(node.node_name, node.status)

    node = client.nodes.resolve("my-node-01")

BarbaraClient is a context manager: using with ensures the underlying HTTP connection pool is closed when you're done. It is not required (a client left open simply keeps the pool alive until garbage collected), but it is good practice in scripts.

Async usage

AsyncBarbaraClient mirrors BarbaraClient method for method: only await differs. Reach for it when you're already inside an async application (a web service, an event loop) rather than as a default choice for one-off scripts.

import asyncio
from barbara import AsyncBarbaraClient

async def main():
    async with AsyncBarbaraClient.from_env() as client:
        nodes = await client.nodes.list()

asyncio.run(main())

The rest of this page shows the synchronous client; every example works identically on the async one by adding await and using async with.

Identifying nodes: internal ID vs. Barbara ID

Every node is addressed internally by an _id (a 24-character hexadecimal string), but nobody remembers those, and the Barbara Panel shows nodes by their human-assigned name (the Barbara ID, e.g. factory-line-3-cam01). This distinction comes up constantly once you start scripting against the API.

node = client.nodes.resolve("factory-line-3-cam01")
print(node.id)  # the internal _id, e.g. "651f3a2e9b1c4d00123abcde"

resolve() looks the name up via a search and returns the matching node, so downstream calls that require an ID (get, reboot, create_global_secrets, ...) have one to use.

Tip

If you already have the internal _id (for instance, saved from a previous run), skip straight to client.nodes.get(node_id). Reserve resolve() for the case where you only have the human-readable name, since it costs an extra search request.

Usage

Nodes

See Node management in Academy for the equivalent Panel actions.

nodes = client.nodes.list(search="sensor")
node = client.nodes.get("<node-id>")
node = client.nodes.resolve("my-node-01")  # look up by node name

client.nodes.reboot("<node-id>")
client.nodes.poweroff("<node-id>")

Node global secrets

Barbara-managed secrets (Wi-Fi pre-shared keys, API tokens injected into a workload, etc.), shared by every workload on the node — distinct from a Marketplace app's own App Secrets and from Docker-native Swarm Secrets. Always base64-encoded by the underlying API. See Secrets in Academy.

client.nodes.create_global_secrets("<node-id>", {"wifi-psk": "s3cr3t"})
secrets = client.nodes.list_global_secrets("<node-id>")
client.nodes.delete_global_secret("<node-id>", "<secret-id>")

Note

The SDK handles the base64 encoding for you: pass plain strings in, get plain strings (or their metadata) back out. You never need to encode a value yourself.

Node global configuration

A free-form JSON document attached to a node, typically read by every workload on it at startup to adjust its behavior without rebuilding it. This corresponds to what the Panel calls Global Config: configuration scoped to the node rather than to a single workload (see Application configuration types for the full picture — a workload's own App Config is set through client.nodes.workloads, below).

client.nodes.set_global_config("<node-id>", config={"threshold": 5})
config = client.nodes.get_global_config("<node-id>")

Docker credentials

client.nodes.create_docker_credentials(
    "<node-id>", [{"user": "bob", "password": "s3cr3t", "server": "docker.io"}]
)

creds = client.nodes.list_docker_credentials("<node-id>")
print(creds[0].server, creds[0].user)  # password is never returned by the API

Node identity: name, tags, location, safety actions

See General info in Academy for the same fields (name, location, tags) as they appear in the Panel.

client.nodes.update_name("<node-id>", "floor-2-sensor-01")
client.nodes.add_tag("<node-id>", "production")

client.nodes.set_location("<node-id>", lat=40.4168, lng=-3.7038, city="Madrid")
location = client.nodes.get_location("<node-id>")

client.nodes.update_safety_actions(
    "<node-id>", trigger_threshold=90, stop_apps=True, prune_volumes=True
)

update_safety_actions configures Panel's "Safety Actions": above trigger_threshold (a percentage, strictly between 0.1 and 99.9), the node can stop its apps/models and/or prune docker resources on its own, without a human watching a dashboard. At least one action must be enabled.

Note

get_location is returned as a raw dict rather than a typed object, since node location payloads vary in shape.

Warning

set_location currently returns a 500 Internal Server Error regardless of the payload sent. Use the Panel to update a node's location until this is resolved.

Barbara Core updates

Barbara Core is the single versioned package that bundles a node's OS and Node Manager together (e.g. Barbara Core 1.10.1.471) — Panel's own update modal shows one version number, not two separate firmwares. See Barbara Core updates in Academy.

client.nodes.update_barbara_core("<node-id>", "update")
client.nodes.update_barbara_core(
    "<node-id>", "schedule", schedule_timestamp="2026-01-01T03:00:00Z"
)
client.nodes.cancel_barbara_core_update("<node-id>")

Docker maintenance and volumes

client.nodes.prune_docker("<node-id>", "prunevolumes")
client.nodes.prune_docker_all("<node-id>")
client.nodes.restart_docker_daemon("<node-id>")

client.nodes.create_docker_volume("<node-id>", "shared-cache")

volumes = client.nodes.list_docker_volumes("<node-id>")
client.nodes.delete_docker_volume("<node-id>", volumes[0]["_id"])

See Volumes in Academy.

Note

create_docker_volume doesn't return an id. Use list_docker_volumes to look one up before calling delete_docker_volume, since node volumes have no dedicated list endpoint of their own.

Telemetry

See Telemetry in Academy.

latency = client.nodes.get_telemetry_latency("<node-id>")
client.nodes.set_telemetry_latency("<node-id>", 30)

telemetry = client.nodes.get_last_telemetry("<node-id>")
print(telemetry["disk"], telemetry["alive"])

Tip

get_last_telemetry and get_telemetry_latency both return raw dicts, with nested fields for disk, network, and containers. Read the keys you need directly.

Node workloads

A workload is an application instance running on one specific node: a Docker app, a Marketplace app, or a Model. Deploying one means pointing at an application version and, for Marketplace/Model apps, describing which services and ports it exposes (its Compose Config). See Docker apps and Marketplace apps in Academy.

client.nodes.workloads.create_docker_workload(
    "<node-id>",
    app_version_id="<app-version-id>",
    application_id="<application-id>",
)

client.nodes.workloads.create_marketplace_workload(
    "<node-id>",
    app_version_id="<app-version-id>",
    application_id="<application-id>",
    name="my-workload",
    compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "9083"}}],
)

client.nodes.workloads.start("<node-id>", "<workload-id>")
client.nodes.workloads.stop("<node-id>", "<workload-id>")
logs = client.nodes.workloads.get_logs("<node-id>", "<workload-id>")

Warning

Creation and update calls do not return the resulting workload state: the API acknowledges the request but does not echo back the deployed object. Call client.nodes.workloads.get(...) afterwards if your script needs to inspect the result (its final status, assigned ports, and so on).

Note

compose_config is a Marketplace/Model-only concept — it's the workload's Compose Config (ports/env/volumes), rewritten into its docker-compose.yml at deploy time. Docker workloads control their own compose file directly, so create_docker_workload/update_docker_workload take no compose_config argument.

A workload's own App Config (as opposed to the node-scoped Global Config above):

client.nodes.workloads.set_app_config("<node-id>", "<workload-id>", config={"threshold": 5})
config = client.nodes.workloads.get_app_config("<node-id>", "<workload-id>")

Model workloads

A model workload is the same idea as a Marketplace workload, but deploying a model application version instead (for example, a TensorFlow Serving or Triton container). It uses the same body shape, with one extra constraint: compose_config must exactly match the service template declared by the selected model application version. See Models in Academy.

client.nodes.workloads.create_model_workload(
    "<node-id>",
    app_version_id="<model-app-version-id>",
    application_id="<model-application-id>",
    name="my-model-workload",
    compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "8501"}}],
)

Clusters

A cluster groups nodes so they can be configured and deployed to together, without repeating the same call once per node. See Clusters in Academy.

clusters = client.clusters.list()
cluster = client.clusters.get("<cluster-id>")
cluster = client.clusters.resolve("floor-2-cluster")  # look up by cluster name
client.clusters.update("<cluster-id>", "new-name")

client.clusters.create_global_secrets("<cluster-id>", {"db-password": "s3cr3t"})
client.clusters.set_global_config("<cluster-id>", config={"threshold": 5})

Creating a cluster and managing membership

Creating a cluster means describing the swarm/VRRP networking for a primary node and, optionally, a set of secondary nodes that join it at creation time.

client.clusters.create(
    "floor-2-cluster",
    primary_node={
        "nodeId": "<node-id>",
        "labels": "eyJ6b25lIjogImZsb29yLTIifQ==",  # base64 JSON: {"zone": "floor-2"}
        "restrictSwarmTrafficToInterface": False,
        "advertiseAddr": "10.0.0.5",
    },
    enable_cluster_volumes=True,
)

Note

primary_node and secondary_nodes take the cluster networking configuration as dicts matching the API schema (nodeId, labels as base64-encoded JSON, advertiseAddr, ...).

Nodes already in a cluster can be moved through its lifecycle states, or join/leave altogether:

client.clusters.join_node(
    "<cluster-id>",
    "<node-id>",
    labels={"zone": "floor-2"},
    restrict_swarm_traffic_to_interface=False,
    advertise_addr="10.0.0.6",
)

client.clusters.pause_node("<cluster-id>", "<node-id>")
client.clusters.drain_node("<cluster-id>", "<node-id>")
client.clusters.set_node_active("<cluster-id>", "<node-id>")
client.clusters.leave_node("<cluster-id>", "<node-id>")

Cluster-wide Docker volumes

Unlike node-level Docker volumes, these give a cluster's workloads Swarm-managed high availability — a workload that needs a volume to survive a node failing over must use one of these.

client.clusters.create_swarm_volume("<cluster-id>", "shared-cache")
client.clusters.delete_swarm_volume("<cluster-id>", "<volume-id>")

Docker-native objects declared in an app's own docker-compose.yml (Swarm Config/Swarm Secrets, distinct from the Barbara-managed Global Config/Global Secrets above) are cleaned up the same way: delete_all_swarm_configs, delete_swarm_config, delete_all_swarm_secrets, delete_swarm_secret.

Cluster workloads

The cluster-level equivalent of node workloads: deploy an application across every node in a cluster in one call, rather than iterating node by node. Barbara's product docs use "Workload" for both scopes — there's no separate "stack" concept in Panel. See Add applications in Academy.

client.clusters.workloads.create_docker_workload(
    "<cluster-id>",
    app_version_id="<app-version-id>",
    application_id="<application-id>",
)

client.clusters.workloads.delete("<cluster-id>", "<workload-id>")

Model workloads work the same way, using create_model_workload, the cluster-level equivalent of create_model_workload on client.nodes.workloads, with the same "compose config must match the model's template" constraint:

client.clusters.workloads.create_model_workload(
    "<cluster-id>",
    app_version_id="<model-app-version-id>",
    application_id="<model-application-id>",
    name="my-model-workload",
    compose_config=[{"name": "modelservice", "ports": {"PORT_NUMBER": "8501"}}],
)

Applications

Applications and their versions form the catalog that workloads are deployed from, at either scope. Creating an application registers its metadata; creating a version uploads the actual installable artifact (typically a Docker image bundle). See App Library in Academy.

apps = client.applications.list()

client.applications.create(
    "edge-app", "Long description", "Barbara", docker=True, icon_path="./icon.png"
)

client.applications.create_version(
    "<application-id>", "./app-v1.tar", "1.0.0", ["amd64"], ["Initial release"]
)

Tip

create and create_version upload files (icon, installable artifact) as multipart/form-data. Pass a local file path. The SDK reads the file from disk and builds the multipart request for you; there is no need to open the file or set headers yourself.

Deploying a Marketplace/Model app from its published template

A Marketplace or Model workload's compose_config/app_secrets must echo back every service declared in the app version's template, or the API rejects the request — default_services builds that starting point from the template's own defaults, ready to hand to create_marketplace_workload/create_model_workload as-is or after overriding individual ports/volumes/env:

app_version = client.applications.get_version("<application-id>", "<app-version-id>")
compose_config, app_secrets = client.applications.default_services(app_version)

client.nodes.workloads.create_marketplace_workload(
    "<node-id>",
    app_version_id="<app-version-id>",
    application_id="<application-id>",
    name="my-workload",
    compose_config=compose_config,
    app_secrets=app_secrets,
)

Models

The model catalog works the same way as the application catalog, but for ML model artifacts deployed to run inference at the edge. See Models in Academy.

models = client.models.list()

client.models.create(
    "anomaly-detector", "Long description", "Barbara", model_type=0, engine=0
)

client.models.create_version("<model-id>", "./model.onnx", "1.0.0", ["Initial release"])

Tip

sha256 and size for a model version are computed automatically from the artifact, so you don't need to hash or measure the file yourself before calling create_version.

Config Repository

Reusable, named configuration documents — typed application or global — that a workload's own App Config or a node's/cluster's own Global Config can reference by id (config_id), rather than being one itself: useful when the same document needs to be attached to many nodes, clusters, or workloads without duplicating it in every call. See Application configuration types in Academy.

config = client.configs.create(
    name="sensor-thresholds",
    description="Per-node alert thresholds",
    config={"temperature_max": 80},
)
print(config.config_type)  # "application" or "global"

client.nodes.set_global_config("<node-id>", config_id=config.id)

Groups

Groups organize nodes independently of clusters: a group is a label for filtering and reporting, and does not imply a shared deployment target the way a cluster does. See Nodes list in Academy for group management in the Panel.

group = client.groups.create(
    name="floor-2-sensors",
    description="All floor 2 nodes",
    node_ids=["<node-id-1>", "<node-id-2>"],
)

Users

Company users, read-only through this API. See Organization in Academy.

users = client.users.list()
page = client.users.paginate(offset=0, size=50)

print(page.items[0].role)  # UserRole.ADMINISTRATOR / SUPERVISOR / EDITOR / VIEWER, or None

Note

paginate exists because list() on some resources caps how many results the API returns in a single call. Use paginate when a company has more users (or, on other resources, more of whatever entity) than fits in one page.

Alerts

See the Alert Manager app in Academy.

alerts = client.alerts.list()
client.alerts.ack("<alert-id>")
events = client.alerts.list_events(node_id="<node-id>")

Roles and permissions

Every Barbara API token carries a role (read, edit, edit_plus, or admin) decoded from the JWT issued at login. These map to the labels Panel shows in the Organization view — Viewer, Editor, Supervisor, Administrator, respectively — but they are not strictly hierarchical: admin does not automatically imply every permission edit_plus has. If a call fails with a permission error, check which role the authenticated user actually holds in the Panel rather than assuming a "higher" role covers it. See Roles and permissions in Academy.

from barbara import BarbaraPermissionError

try:
    client.nodes.reboot("<node-id>")
except BarbaraPermissionError:
    print("The authenticated user's role does not allow this action")

Error handling

All API errors raise a subclass of BarbaraApiError, so you can catch broadly or narrow down to a specific failure mode:

from barbara import BarbaraApiError, BarbaraAuthError, BarbaraNotFoundError, BarbaraPermissionError

try:
    client.nodes.resolve("unknown-node")
except BarbaraNotFoundError:
    ...
except BarbaraPermissionError:
    ...
except BarbaraApiError as e:
    print(e.status, e.body)

Note

A 404 on an optional sub-resource (for example, a node with no secrets configured yet) is often expected, not an error condition worth aborting a script over. Decide, call by call, whether BarbaraNotFoundError should stop your program or just be logged and skipped.

Architecture

  • One client, one resource tree. BarbaraClient and AsyncBarbaraClient expose the same resources (.nodes, .clusters, .applications, ...) with identical method signatures.
  • Automatic token refresh. A request that receives a 401 is retried once with a freshly fetched token.
  • Typed models. Response entities are plain dataclasses. Every entity keeps the original API payload in .raw, so a field the SDK hasn't typed yet is still reachable.
  • Typed exceptions. BarbaraNotFoundError, BarbaraAuthError, and BarbaraPermissionError subclass BarbaraApiError so callers can handle specific failure modes without inspecting status codes by hand.
  • Product naming throughout. Classes, methods, and fields follow Barbara's own product terminology (Node, Workload, App Config vs. Global Config, Barbara Core, ...) rather than internal API/wire jargon — see the Barbara Academy links throughout this page for the concepts behind each resource.
  • An escape hatch for everything else. Every resource method calls client.request(method, path, ...) internally, and the same authenticated, token-refreshing request method is available directly for any endpoint not yet wrapped by a typed resource. When building path yourself, percent-encode any value that isn't a fixed literal (urllib.parse.quote(value, safe="")). Every typed resource method does this for its own id parameters, but client.request(...) takes path as-is.
# Calling an endpoint the SDK doesn't wrap yet, using the same authenticated
# request method every typed resource is built on:
response = client.request("GET", "/v1/some/future/endpoint")

API reference

Resource Description
client.nodes Node lifecycle, global secrets, global config, docker credentials, Barbara Core updates, and actions (reboot, provision, ...)
client.nodes.workloads Docker/Marketplace/Model applications deployed on a node
client.clusters Cluster lifecycle, global secrets, global config, docker credentials, and Swarm-managed volumes
client.clusters.workloads Docker/Marketplace/Model applications deployed across a cluster
client.applications Application catalog and versions
client.models Model catalog and versions
client.configs Config Repository — reusable, named configuration documents
client.groups Node groups
client.users Company users (read-only)
client.alerts Alerts and alert events

Use the navigation sidebar for the full generated reference: every method, parameter, and return type. For the underlying HTTP API itself, see the Barbara API documentation.

Examples

The examples/ directory has complete, runnable scripts for common use cases:

Script Description
hello_world.py The first script to run: confirm your credentials work and list your nodes
node_info.py Read a node's configuration and latest telemetry
check_barbara_core_updates.py Check nodes for an outdated Barbara Core, optionally update them
clone_node.py Clone a node's workloads, config, and docker volumes onto another node

Each script also demonstrates calling an endpoint through client.request(...) directly (the same low-level method every typed resource is built on) for functionality this SDK doesn't wrap yet.

See the examples README for what each one covers, how to configure and run it, and ideas for extending it.

Roadmap

The following areas of the Barbara API are not yet covered by this SDK:

  • Node network configuration (interfaces, NTP, proxy, VPN, iptables)
  • Node standalone mode and VPN peer management
  • App Secrets (Marketplace-only, per-app secrets, distinct from a node's Global Secrets)

License

Distributed under the MIT License. See LICENSE for details.