Why This Post Exists
If you've worked with BlueCat Address Manager's REST API, you've probably used the v1 endpoints under /Services/REST/v1/. Those examples are all over the internet -- including BlueCat's own making-apis-work-for-you repo on GitHub, which was the go-to reference for years.
The problem: v1 is deprecated. BlueCat introduced the RESTful v2 API in BAM 9.5.0, and the v1 API is now officially called the "Legacy v1 API." If you're writing new integrations or maintaining existing ones, you need to migrate.
I went looking for an updated version of those examples and found that nobody had done it. The original making-apis-work-for-you repo hasn't been meaningfully updated since 2018. The example scripts are a mix of Python 2 and Python 3, and the REST examples all target the v1 API. BlueCat shipped the v2 API years ago, published migration guides, even released an official Python SDK (bluecat-libraries) -- but the reference examples that most people find first on Google and GitHub were never brought forward. There are a handful of community wrappers on GitHub (see pyBluecat, py-bluecat-bam-sdk), but none of them provide the kind of simple, runnable, "Episode 1 through 7" walkthrough that the original repo did.
So I built one. I rewrote every example from the original BlueCat repo using the v2 API in both Python and TypeScript, added an office network template provisioner that didn't exist before, and open-sourced it all under Apache 2.0: elephantatech/bluecat-bam-v2-examples. This post walks through the biggest changes, with code you can copy.
Official docs
Here are the docs you'll want bookmarked:
- BAM RESTful v2 API Guide (25.1.0) -- the primary reference
- BAM Legacy v1 API Guide (9.5.0) -- if you're migrating off v1
- v1 to v2 Migration Guide -- BlueCat's official migration reference
- BlueCat Python Library (bluecat-libraries) -- official Python SDK
- bluecat-libraries on PyPI -- official package, published and maintained by BlueCat Networks
Every BAM instance also ships with interactive Swagger docs at https://<your-bam>/api/docs and the OpenAPI 3.0 spec at https://<your-bam>/api/openapi.json.
Before you write any code: BAM-side setup
The API is always on -- there's no switch to enable it. But your user account has to be configured correctly or every call will fail with a 401. This trips people up because a user who works fine in the web UI might not have API access at all.
Create a dedicated API user
In the BAM web UI, go to Administration > Users and Groups and create a new user. The fields that matter (full docs: Creating an API user in Address Manager):
- Username -- something like
svc-provisionerorapi-automation. Don't reuse a personal account for scripts. - Access Type -- this is the one that bites you. There are three options:
- GUI -- web UI only. Cannot use the API at all.
- API -- API only. Cannot log in to the web UI.
- GUI and API -- both.
- User Type -- Administrator has access to everything (DNS, IPAM, DHCP, system settings). Non-Administrator is limited to DNS and IPAM only and needs explicit access rights on every object it should touch.
Set access rights
If you're using a Non-Administrator user, you need to grant access rights on the objects the user will work with. Access rights are hierarchical -- they're set per object (Configuration, Block, View, Zone, Network) and inherit down the tree. The levels are:
- View -- read-only
- Add -- can create child objects
- Change -- can modify objects
- Full Access -- create, modify, delete
For a provisioner that creates networks, zones, and DHCP scopes, you need at least Full Access on the target Configuration. Or you can be more granular and set Add + Change on specific Blocks and Views. An Administrator user skips all of this -- it has implicit full access everywhere.
Other things to check
- HTTPS -- BAM ships with a self-signed certificate. Your API client will reject it unless you either import the cert into your trust store or disable verification (
verify=Falsein Python,rejectUnauthorized: falsein Bun). For production, get a proper CA-signed cert on the BAM -- configure it under Administration > HTTPS Configuration. - Session timeout -- the API token lifetime is tied to the BAM session inactivity timeout. The v2 session response includes
apiTokenExpirationDateTime(usually 24 hours). If your script runs longer than the timeout, you'll need to re-authenticate. - Firewall -- BAM has a built-in firewall under Administration > Firewall. There's no API-specific IP allowlist, but you can restrict which IPs can reach BAM on port 443. If your automation server is getting connection refused, check here.
- No rate limiting -- BAM doesn't throttle API requests. But it's an appliance with finite resources, so don't hammer it with hundreds of concurrent sessions. Reuse your session and call logout when you're done.
What changed
| Area | v1 (Deprecated) | v2 (Current) |
|---|---|---|
| Base URL | /Services/REST/v1/ |
/api/v2/ |
| Auth | GET with credentials in URL | POST with JSON body |
| Token format | Parse from string: "Session Token-> BAMAuthToken: abc123" |
JSON field: {"apiToken": "abc123"} |
| API style | RPC: POST /v1/addHostRecord?viewId=123&absoluteName=... |
REST: POST /api/v2/zones/456/resourceRecords |
| Response format | Pipe-delimited properties: "name=x|connected=true|" |
Clean JSON objects |
| Hierarchy traversal | getEntityByName calls chained manually |
Resource paths: /configurations/{id}/views/{id}/zones |
| Documentation | Static docs only | OpenAPI 3.0 spec + Swagger UI on every BAM instance |
BlueCat's Official Python SDK: bluecat-libraries
BlueCat does have an official Python SDK. The bluecat-libraries package on PyPI is published and maintained directly by BlueCat Networks. I verified this -- the PyPI metadata lists "BlueCat" as both author and maintainer, the homepage points to docs.bluecatnetworks.com, and the copyright is "BlueCat Networks (USA) Inc. and its affiliates and licensors." It covers the v2 API, the legacy v1 API, Failover, DNS Edge, and Micetro. Latest version is 25.3.0 (November 2025), requires Python 3.11+, Apache 2.0 licensed.
pip install bluecat-libraries
So why did I write my own client? The official SDK is the right choice for production systems, but it hides what's happening over the wire. My examples use raw requests calls in a thin wrapper so you can see every URL, every JSON body, every header -- which makes it better for learning the API. There's also no TypeScript/Node.js SDK from BlueCat at all, so the Bun examples in this repo fill that gap.
Not every project needs the full library either. If you're writing a small cron job that registers a few DNS records or a provisioning hook that grabs the next available IP for a new VM, a single-file client with just requests is simpler to drop into an existing project and easier for your team to read without studying SDK docs. For lightweight automation, a thin wrapper is all you need. For production IPAM tooling or multi-API workflows across BAM and DNS Edge, use bluecat-libraries.
Authentication: where it actually breaks
If anything trips you up during migration, it'll be auth. The v1 and v2 approaches have nothing in common.
v1 auth (old way)
In v1, you send credentials as GET parameters in the URL. Yes, really. And the response is a raw string you have to parse:
# v1: Credentials in the URL (!) + string parsing
import requests
bam_url = "https://bam.lab.corp"
login_url = f"{bam_url}/Services/REST/v1/login?username=api&password=pass"
# Credentials sent as GET parameters -- visible in logs, browser history, proxies
response = requests.get(login_url)
# response.json() returns a plain string in v1, not a dict
# It looks like: "Session Token-> BAMAuthToken: abc123 ..."
# You have to split it manually to extract the token
token = str(response.json())
token = token.split()[2] + " " + token.split()[3]
# Then set it as a header for subsequent calls
header = {'Authorization': token, 'Content-Type': 'application/json'}
# ... make API calls ...
# Logout
requests.get(f"{bam_url}/Services/REST/v1/logout?", headers=header)
The obvious problems: your password is in the URL, which means it shows up in server logs, proxy logs, and browser history. The token comes back as a raw string you have to split on whitespace. And if auth fails, you get no structured error -- just a string.
v2 auth (new way)
In v2, you POST JSON to a sessions endpoint and get back actual JSON:
# v2: JSON body + structured response
import requests
bam_url = "https://bam.lab.corp"
# Credentials sent in POST body -- not in the URL
resp = requests.post(
f"{bam_url}/api/v2/sessions",
json={"username": "admin", "password": "pass"},
)
resp.raise_for_status()
data = resp.json()
# Token is a clean JSON field -- no string parsing
token = data["apiToken"]
# The response also includes:
# data["apiTokenExpirationDateTime"] -- when the token expires (~24h)
# data["basicAuthenticationCredentials"] -- pre-encoded base64 for Basic auth
# Use Bearer auth for subsequent calls
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
})
# ... make API calls ...
# Logout
session.delete(f"{bam_url}/api/v2/sessions")
Credentials stay in the POST body, not the URL. Token is a JSON field you can just read. You also get an expiry timestamp so you know when to refresh, and proper HTTP status codes when something goes wrong. Both Bearer and Basic auth work.
Same thing in TypeScript / Bun
// v2 auth in TypeScript (Bun)
const resp = await fetch("https://bam.lab.corp/api/v2/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "admin", password: "pass" }),
});
const { apiToken } = await resp.json();
// All subsequent requests use Bearer token
const data = await fetch("https://bam.lab.corp/api/v2/configurations", {
headers: { Authorization: `Bearer ${apiToken}` },
}).then(r => r.json());
A simple v2 client
In the original repo, every script repeated the same boilerplate -- URL construction, token parsing, header building. With v2, you can wrap all of that once and forget about it:
import requests
class BAMClient:
def __init__(self, url, username, password):
self.api_url = f"{url.rstrip('/')}/api/v2"
self.session = requests.Session()
self.username = username
self.password = password
def login(self):
resp = self.session.post(
f"{self.api_url}/sessions",
json={"username": self.username, "password": self.password},
)
resp.raise_for_status()
self.session.headers.update({
"Authorization": f"Bearer {resp.json()['apiToken']}",
"Content-Type": "application/json",
})
def logout(self):
self.session.delete(f"{self.api_url}/sessions")
def __enter__(self):
self.login()
return self
def __exit__(self, *args):
self.logout()
def get(self, path, params=None):
resp = self.session.get(f"{self.api_url}{path}", params=params)
resp.raise_for_status()
return resp.json() if resp.content else None
def post(self, path, data=None):
resp = self.session.post(f"{self.api_url}{path}", json=data)
resp.raise_for_status()
return resp.json() if resp.content else None
Usage:
with BAMClient("https://bam.example.com", "admin", "password") as bam:
configs = bam.get("/configurations")
for cfg in configs["data"]:
print(f" [{cfg['id']}] {cfg['name']}")
Endpoint changes, side by side
Adding a host record
In v1 you had to walk the hierarchy with getEntityByName calls, build URLs with string concatenation, and pass everything as query parameters:
# v1: Walk Config -> View, then construct URL manually
getEntityByName = mainurl + "getEntityByName?parentId=" + str(e_parentId) \
+ "&name=" + e_name + "&type=" + e_type
addHostRecord = mainurl + "addHostRecord?viewId=" + str(r_viewId) \
+ "&absoluteName=" + r_absoluteName \
+ "&addresses=" + r_addresses \
+ "&ttl=" + r_ttl \
+ "&properties=reverseRecord=true"
response = requests.post(addHostRecord, headers=header)
v2 -- resource paths with JSON bodies. The hierarchy traversal is still there, but it reads like actual REST:
# v2: Clean REST resource paths
with BAMClient(url, user, password) as bam:
config = bam.get("/configurations", params={"filter": "name:eq('main')"})
config_id = config["data"][0]["id"]
view = bam.get(f"/configurations/{config_id}/views", params={"filter": "name:eq('default')"})
view_id = view["data"][0]["id"]
# Find zone, then create host record -- JSON body, not query string
zones = bam.get(f"/views/{view_id}/zones")
zone_id = zones["data"][0]["id"]
bam.post(f"/zones/{zone_id}/resourceRecords", data={
"type": "HostRecord",
"name": "FINRPT02",
"addresses": [{"address": "192.168.0.16"}],
"reverseRecord": True,
})
DHCP reservation
This one was always painful in v1. The assignNextAvailableIP4Address call needed host info packed into a comma-delimited string, and properties as pipe-delimited key-value pairs:
# v1: Encoding nightmare
hostInfo = hostname + "." + zonename + "," + str(viewinfo['id']) + ",true,false"
params = {
"configurationId": config_id,
"parentId": network_id,
"macAddress": "BB:CC:DD:AA:AA:AA",
"hostInfo": hostInfo,
"action": "MAKE_DHCP_RESERVED",
"properties": "name=appsrv23|locationCode=US DAL|",
}
response = requests.post(assignNextAvailableIP4Addressurl, params=params, headers=header)
v2 -- just JSON:
# v2: Just JSON
bam.post(f"/networks/{network_id}/nextAvailableAddress", data={
"action": "MAKE_DHCP_RESERVED",
"macAddress": "BB:CC:DD:AA:AA:AA",
"name": "appsrv23",
})
Server properties: no more pipe parsing
This one drove me crazy. In v1, server properties came back as a single pipe-delimited string and you had to parse them into a dict yourself:
# v1: Parse pipe-delimited properties manually
for server in serverslist:
propertieslist = list(server['properties'].split("|"))
propertiesdic = {}
for item in propertieslist:
if item is not '':
shortlist = list(item.split("="))
propertiesdic[shortlist[0]] = shortlist[1]
server.update(propertiesdic)
del server['properties']
# v2: Properties are just JSON fields. That's it.
servers = bam.get(f"/configurations/{config_id}/servers")
for server in servers["data"]:
print(f" {server['name']} - connected: {server.get('connected')}")
Office template provisioner
I also added something that wasn't in the original repo at all: a template provisioner. You describe your office in a YAML file -- VLANs, subnets, DHCP pools, hardware MAC addresses, WiFi SSIDs -- and the script creates everything in BAM.
It runs in dry-run mode by default, so you can see what it would create without a BAM instance:
$ uv run python examples/07_office_template.py
[DRY RUN] Provisioning: Chicago Office 1 (chi1)
Domain: chi1.corp.example.com
Supernet: 10.40.0.0/20
1. Create IP block: 10.40.0.0/20
2. Create networks:
VLAN 10 | 10.40.0.0/23 | chi1-vlan10-Corp-Data
VLAN 30 | 10.40.4.0/22 | chi1-vlan30-Corp-WiFi
VLAN 40 | 10.40.8.0/23 | chi1-vlan40-Guest-WiFi
5. Hardware provisioning:
switches | sw-core01 | 10.40.10.129 | AA:BB:CC:01:01:01
wireless_aps | ap-1f-01 | 10.40.10.140 | AA:BB:CC:02:01:01
printers | pr-1f-01 | 10.40.0.20 | AA:BB:CC:03:01:01
[DRY RUN] Total actions: 25
Try it out
Python (uv)
git clone https://github.com/elephantatech/bluecat-bam-v2-examples.git
cd bluecat-bam-v2-examples/python
uv sync
uv run python examples/07_office_template.py
TypeScript (Bun)
cd ../nodejs
bun install
bun examples/04-office-template.ts
When you have a BAM instance
export BAM_URL="https://your-bam.example.com"
export BAM_USER="admin"
export BAM_PASS="your-password"
DRY_RUN=false uv run python examples/07_office_template.py
The short version
Stop using v1. BlueCat calls it "Legacy" now. Auth is POST with JSON instead of credentials in the URL. Responses are real JSON instead of pipe-delimited strings. Endpoints are REST resource paths instead of RPC-style query strings. Every BAM instance has Swagger UI at /api/docs and an OpenAPI spec at /api/openapi.json so you can generate clients in any language.
For production Python, use BlueCat's official bluecat-libraries SDK -- it's maintained by BlueCat Networks and covers v2, failover, DNS Edge, and Micetro. For lightweight scripts or TypeScript, there's no official option. The clients in this repo are a good starting point.
The full project with both Python and TypeScript examples is on GitHub under Apache 2.0: elephantatech/bluecat-bam-v2-examples
If you spot something wrong or want to add an example, open an issue or PR.