On this page
The API
LiteBlox has a JSON API under /api/v1. Scripts, monitoring and other tools use it to read nodes, zones, records and DHCP leases, and to change records and DHCP reservations. Every endpoint is listed, with its fields, on the API page (/api/docs), which any logged-in user can open. Calls go through the same checks as the pages, so a publish is still reviewed against named-checkzone and the nodes, and a DHCP change still goes to the standby first and rolls back if Kea refuses it.
Tokens
Every call needs an API token, sent as a header:
Authorization: Bearer lbx_…
- Where: your own tokens are on My account → API tokens. Admins make and revoke anyone's on the Users tab, which also suits a service account (a
readonlyuser calledmonitoring, say). From the command line:liteblox token add USER NAME [--read-only] [--expires DAYS]. - Scope:
- read only: GET calls only.
- full: whatever the token's user may do. An admin's full token can change things; a readonly user's full token still only reads.
- The token is shown once, when it's made. LiteBlox keeps only a hash of it: if it's lost, revoke it and make another.
- The user's role is checked on every call, so a role change or a deleted user applies at once. Changing a password doesn't revoke tokens: revoke them on the same page.
- An optional expiry (in days); the token list shows when each one was last used, and from where.
- The API takes only tokens. A browser's login session isn't accepted on
/api/v1, and a token doesn't log anyone into the pages.
Trying it
On the API page, Authorize with a token, then Try it out on any endpoint. From a shell ($LB is LiteBlox's address, $TOKEN the token):
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/me
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/zones
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/zones/example.com/records
Records
A record's id comes from its content (owner, type and data), so it stays the same while the record does, whatever else changes in the zone. Changing a record gives it a new id, which the answer includes.
A static zone changes in its draft, as in the grid. Look at the draft, then publish it:
# add a record (and its PTR, when the zone's auto-PTR is on)
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/zones/example.com/records \
-d '{"name": "web", "type": "A", "data": "192.168.0.20"}'
# what publishing would change: added/removed/changed records, the diff, named-checkzone, linked PTR drafts
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/zones/example.com/draft
# publish it (with the PTR drafts linked to it); the answer is a job to poll
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/zones/example.com/publish -d '{"message": "Add web"}'
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/jobs/42
datais the record data as a zone file writes it:10 mail.example.com.for an MX, TXT as"quoted strings".ttlis seconds or1h/30m; leave it out for the zone's default.PATCH /zones/{zone}/records/{id}changes only the fields you send.DELETEtakes?ptr=falseto leave the PTR alone.PUT /zones/{zone}/soachanges the SOA's names and timers. The serial stays LiteBlox's.DELETE /zones/{zone}/draftdiscards the draft, with its linked PTR drafts.PUT /zones/{zone}/noteswith{"notes": "…"}replaces the zone's notes;GET /zones/{zone}returns them. They're kept in LiteBlox only, so this works for any zone and nothing is published.GET /zones/{zone}/textreturns the zone file.GET /zones/{zone}/historylists its versions, andGET /zones/{zone}/history/{sha}returns one version's text.
A dynamic zone changes at once, as one guarded update to its primary, as a grid edit does. It has no draft and nothing to publish.
Many changes at once
POST /zones/{zone}/records/batch takes a list of ops and makes them as one change: all of them, or none.
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/zones/example.com/records/batch -d '{
"ops": [
{"op": "add", "name": "web1", "type": "A", "data": "192.168.0.21", "comment": "rack 2"},
{"op": "add", "name": "web2", "type": "A", "data": "192.168.0.22", "ttl": "10m"},
{"op": "change", "id": "<an id from GET …/records>", "ttl": 600},
{"op": "delete", "id": "<another id>"}
]}'
addneedsname,typeanddata;ttlandcommentare optional.changetakes anidand only the fields to change, asPATCHdoes.deletetakes anid. Ids are the records' ids before the batch, and each record can appear in one op only. Up to 2000 ops.- Each op is checked against the zone as the ops before it leave it, the way Add many checks its lines. If any op isn't valid, nothing changes, and the 422 lists every bad op:
{"detail": …, "errors": [{"index": 1, "errors": {"value": …}}]}. - The answer has
records, one per op in order: the record each op became, with its new id (nullfor a delete). It also has the zone's newrev, and any notes and warnings. - A static zone gets them all in its draft. Publish it as usual. PTRs follow A records unless
"ptr": false, and the reverse zones' drafts are linked so they publish together. - A dynamic zone gets one update. If a record's set changed on the server meanwhile, the update is refused whole (409).
If-Matchtakes the zone'srev. In a dynamic zone it can instead list therrof every record the batch changes or deletes.
Changes made meanwhile
GET …/recordsreturns the zone'srev(also as theETagheader). Send it back asIf-Matchon a write, and LiteBlox refuses the write with 412 if the zone changed since. In a static zone, publishing withIf-Matchrefuses a draft that changed after you looked at it.-
In a dynamic zone, DHCP changes the zone all the time, so each record also has an
rrtoken. AsIf-Match, it checks only that record's set (its name and type), so leases landing elsewhere don't get in the way:curl -s -H "Authorization: Bearer $TOKEN" -H "If-Match: \"$RR\"" -H 'Content-Type: application/json' \ -X PATCH $LB/api/v1/zones/ddns.lab/records/$ID -d '{"data": "192.168.0.31"}' -
Without
If-Match, a write still never hits the wrong record, since ids are by content. Two writes to one zone don't interleave, whether from the pages or the API.
Jobs
Publishing and DHCP changes run as jobs, as they do from the pages. The call answers 202 with {"job": N} (and a Location header). Poll GET /api/v1/jobs/N until status is ok or failed; its log is what the job page shows. GET /api/v1/jobs lists recent ones.
DHCP
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/dhcp # servers, the failover pair, settings
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/dhcp/subnets # with their reservations
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/dhcp/leases # live from Kea (?subnet=ID)
# reserve an address (a job: every DHCP node, the standby first)
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/dhcp/subnets/2/reservations \
-d '{"hw_address": "aa:bb:cc:dd:ee:ff", "ip_address": "192.168.0.60", "hostname": "printer"}'
# delete a lease now (its DNS names go too; the client asks again at its next renewal)
curl -s -H "Authorization: Bearer $TOKEN" -X DELETE $LB/api/v1/dhcp/leases/192.168.0.123
- A reservation without
ip_address(or with"") is name-only, in a subnet that registers clients and with ahostname. - A reservation's answer lists
warnings, for example another client holding the address right now.PATCHchanges only the fields you send;DELETEremoves it. A failover pair's leases are read from its primary, or from the standby when the primary doesn't answer. - The settings in
GET /dhcpinclude where clients are registered in DNS:ddns_mode(liteblox,externaloroff), and forexternaltheddns_serverandddns_port(never the key's secret). A subnet'sddns(zone,suffix) is where its clients go under that mode (nullwhen it registers nothing). - Only one DHCP change runs at a time: another one gets 409 until the running job has finished.
Search
GET /api/v1/search?q=… is the Search page as JSON: records, reservations, leases, zones, subnets and devices, each with its count and up to limit (default 200) items. A query for one IPv4 address also gets address: its names, PTR, reservation, lease, subnet, and the checks that didn't add up. A dynamic zone searched from its history because its primary didn't answer gives its records as_of; what a short query didn't read live is in skipped. A read-only token is enough.
curl -s -H "Authorization: Bearer $TOKEN" "$LB/api/v1/search?q=192.168.0.50" | jq .address
IPAM
Everything on the IPAM pages, under /api/v1/ipam: networks (GET, POST, PATCH, DELETE) and their ranges, a network's addresses (one /24 per page, used_only to leave the free ones out), what the nodes seen (and DELETE /ipam/seen/{ip} to forget it), flapping addresses and their ack, and devices (PUT a label and comment).
Next free finds addresses nothing uses and holds them for a few minutes, then assign puts names on them:
# the next 3 free addresses in a row, held for 10 minutes
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/ipam/next-free -d '{"network": "192.168.10.0/24", "count": 3}'
# A records (and PTRs) for them, published; a group to remove them by later
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -X POST $LB/api/v1/ipam/assign \
-d '{"ips": ["192.168.10.81", "192.168.10.82", "192.168.10.83"], "name": "k8s-{n}", "zone": "lab.example.com", "group": "k8s"}'
# and gone again: only what assign made
curl -s -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-X POST $LB/api/v1/ipam/unassign -d '{"group": "k8s"}'
next-freetakes one ofnetwork(an IPAM network's id or prefix),range(a range's id) orsubnet(a DHCP subnet: outside its pools, for a reservation), andcount(1 to 64),align,hold,minutes. Its answer says whether the addresses werecheckedon the network, and gives thehold_id.DELETE /ipam/holds/{ip}orDELETE /ipam/holds?hold_id=releases them early.assignwith amac(ormacs, one per address) makes DHCP reservations, with the name as their hostname; without, A records inzone(or the zone the full name is in), then a publish unlesspublishis false. With"target": "ipam"(the default when the DNS module is off) it makes IPAM records instead of A records: nothing goes to DNS, and a MAC outside the DHCP subnets stays on the record. Its answer lists thejobs, and what each address got inkinds(static,reservation,ipam).namesare the IPAM records:GET /ipam/names(networkto narrow it),GET,PUT(name,mac,comment) andDELETE /ipam/names/{ip}. A network'saddressesgive each one'sipam_name, and itsrdnswhen a resolver is set (PTR lookups).unassigntakesips, arange("192.168.10.81-192.168.10.83") or agroup, and removes only whatassignmade: anything else at those addresses is listed inskipped.
These need a full token; the reads work with a read-only one.
The command line (lbcli)
lbcli.py does the same from a shell: one file, Python 3.9 or later, nothing to install. It reads the server from LITEBLOX_URL (or --url) and the token from LITEBLOX_TOKEN (or --token-file).
export LITEBLOX_URL=https://liteblox.example.com LITEBLOX_TOKEN=lbx_…
lbcli.py next --subnet 192.168.1.0/24 --blocksize 3 # 192.168.1.81, 192.168.1.82, 192.168.1.83
lbcli.py reserve 'k8s-{n}' --network 192.168.10.0/24 --blocksize 3 --zone lab.example.com --group k8s
lbcli.py reserve printer --subnet 192.168.1.0/24 --mac aa:bb:cc:dd:ee:ff # a DHCP reservation
lbcli.py reserve nas --network 192.168.1.0/24 --ipam # a name kept in IPAM only
lbcli.py unassign --group k8s
lbcli.py release --hold 3f9c0a1b2c4d
lbcli.py show 192.168.1.20
reserve takes the next free addresses (or --ip) and assigns them in one go; --json prints the API's answer for scripts. Errors start with ERROR: and exit 1.
Time
GET /api/v1/ntp returns the sources every node uses and, for each node with time sync, whether it serves time, to which networks, and chrony's last state (synced, the source it follows, stratum, offset). Each node in GET /api/v1/nodes carries the same state as time.
Status
GET /api/v1/status is the Dashboard as JSON, for a monitoring system: level (ok, warn or error), attention (how many problems need an admin), checked_at (when the health check last heard from a node, UTC), the problems (each with severity, area, what, the page that fixes it as link, and the node), and the tiles. A read-only token is enough.
curl -s -H "Authorization: Bearer $TOKEN" $LB/api/v1/status | jq -r .level
Errors
| Status | Meaning |
|---|---|
| 401 | no token, or it's unknown, revoked or expired |
| 403 | the token is read-only, or its user isn't an admin |
| 404 | no such zone, record, subnet, reservation, lease, network or job |
| 409 | not possible as things are: nothing free (left) for next free, an address already assigned, a forwarding zone has no records, a dynamic zone has no draft, a publish or DHCP change is already running, or a dynamic zone changed on its primary during the update |
| 412 | If-Match didn't match: read it again |
| 422 | not valid. errors says which field and why ({"detail": …, "errors": {"value": …}}) |
| 502 | a node or a zone's primary didn't answer |
Every other answer is JSON, with detail saying what went wrong.
What it doesn't do (yet)
These are on the pages: - creating and deleting zones, and zone settings - nodes (setup, take over, server settings, promote) - TSIG keys and ACLs - DHCP subnets, settings, interfaces and the failover pair
Every write through the API is in the audit log under the token's user, with the token's name.