
Unifly
- 104 installs
- 236 repo stars
- Updated August 4, 2026
- hyperb1iss/unifly
Helps with ai & agent building tasks.
About
unifly is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- unifly
- AI & Agent Building
- AI-coding skill
Unifly by the numbers
- 104 all-time installs (skills.sh)
- +13 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #4,243 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hyperb1iss/unifly --skill uniflyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 104 |
|---|---|
| repo stars | ★ 236 |
| Last updated | August 4, 2026 |
| Repository | hyperb1iss/unifly ↗ |
What it does
Helps with ai & agent building tasks.
Files
unifly: UniFi Network Management
unifly is a Rust CLI for managing Ubiquiti UniFi network infrastructure. It unifies the modern Integration API (REST, API key), the Session API (cookie plus CSRF), and Site Manager cloud APIs behind a single coherent interface, plus real-time WebSocket event streaming. 28 top-level commands cover devices, clients, networks, WiFi, firewall policies and zones, NAT policies, ACLs, DNS, traffic matching lists, hotspot vouchers, DPI, stats, backups, cloud fleet queries, and a raw API escape hatch.
Unique capabilities worth leading with when the user's task suits them:
- Dual-API enrichment merges Integration and Session data (e.g. client
bytes, hostnames, uplink MACs only exist in the Session API; configuration CRUD only exists in Integration). On UniFi OS, API key mode can already reach session HTTP; Hybrid adds the WebSocket session for live monitoring.
- Real-time event streaming via
unifly events watchover WebSocket. - Firewall policy reordering via
reorder --get/reorder --setfor
deterministic, round-trippable ordering edits.
- `unifly api` raw passthrough for endpoints unifly does not wrap.
- Multi-profile (
-p home,-p office) for managing multiple controllers
from one command line.
Prerequisites
Verify availability before running any command:
command -v unifly >/dev/null 2>&1 && unifly --version || echo "unifly not installed"If unifly is not installed, prefer brew install hyperb1iss/tap/unifly on macOS or cargo install --git https://github.com/hyperb1iss/unifly.git unifly elsewhere. After install, run unifly config init for a local controller or unifly config cloud-setup for Site Manager. See examples/config.toml for manual configuration.
Authentication Modes
unifly supports four modes. API key mode is enough for most HTTP automation on UniFi OS controllers. Choose Hybrid when the task needs live WebSocket features (events watch) or you want maximum compatibility.
| Mode | Credentials | What It Unlocks |
|---|---|---|
integration | API key | Integration API plus session HTTP on UniFi OS: CRUD, device commands, stats, reservations, admin, event list |
session | Username + password | Session HTTP + WebSocket only: events watch, stats, device commands, DPI control, admin, backups |
hybrid | API key + username/pass | Everything above, including session WebSocket plus enriched records with maximum controller compatibility |
cloud | Site Manager API key | Connector-routed Integration CRUD plus unifly cloud fleet commands against api.ui.com |
Session WebSocket still rejects API keys, so events watch needs session or hybrid. Cloud mode does not expose Session API endpoints or WebSocket streaming.
For the complete command-to-API gate matrix (which commands require which auth mode), consult references/concepts.md.
Command Inventory
All commands follow unifly [global-flags] <command> <action> [args].
| Command | Aliases | Actions |
|---|---|---|
devices | dev, d | list, get, adopt, remove, restart, locate, port-cycle, ports, ports-export, port-set, stats, pending, upgrade, provision, speedtest, tags |
clients | cl | list, find, get, roams, wifi, authorize, unauthorize, block, unblock, kick, forget, reservations (res), set-ip, remove-ip |
cloud | hosts [get], sites, switch, devices, isp [query], sdwan [get, status] | |
networks | net, n | list, get, create, update, delete, refs |
wifi | w | list, get, neighbors, channels, create, update, delete |
firewall | fw | policies {list, get, create, update, patch, delete, reorder}, zones {list, get, create, update, delete}, groups {list, get, create, update, delete} |
nat | policies {list, get, create, update, delete} | |
acl | list, get, create, update, delete, reorder | |
dns | list, get, create, update, delete | |
traffic-lists | list, get, create, update, delete | |
hotspot | list, get, create, delete, purge | |
events | list, watch | |
alarms | list, archive, archive-all | |
stats | site, device, client, gateway, dpi | |
dpi | apps, categories, status, enable, disable | |
topology | topo | _(no subcommands)_ |
system | sys | info, health, sysinfo, backup {create, list, download, delete}, reboot, poweroff |
settings | list, get, set, export | |
sites | list, create, delete | |
admin | list, invite, revoke, update | |
wans | list | |
vpn | servers {list, get}, tunnels {list, get}, status, health, site-to-site {list, get, create, update, delete}, remote-access {list, get, create, update, suggest-port, download-config, delete}, clients {list, get, create, update, delete}, connections {list, get, restart}, peers {list, get, create, update, delete, subnets}, magic-site-to-site {list, get}, settings {list, get, set, patch} | |
radius | profiles | |
countries | _(no subcommands)_ | |
api | Raw API passthrough (GET/POST/PUT/PATCH/DELETE any path) | |
config | init, cloud-setup, show, set, profiles, use, set-password | |
tui | _(no subcommands)_ | |
completions | bash, zsh, fish, powershell, elvish |
For flag details and gotchas, consult references/commands.md. Every entity command accepts --help at runtime as the authoritative reference.
Output Formats
All list and get commands accept --output / -o:
| Format | Flag | Use Case |
|---|---|---|
table | -o table | Human display (default) |
json | -o json | Agent processing, pipe to jq |
json-compact | -o json-compact | Single-line JSON for scripting |
yaml | -o yaml | Config file output |
plain | -o plain | One ID per line for xargs pipelines |
Default for agent use: `-o json`. Emit structured output, pipe through jq, and only fall back to table when the result is being shown to a human.
Power Patterns
These patterns unlock unifly's most distinctive capabilities. For full recipes with runnable shell scripts, consult references/workflows.md.
--from-file for complex create/update
Most entities accept --from-file <path.json> (or -F) instead of flag salad: networks, wifi, firewall policies, firewall zones, firewall groups, nat policies, acl, dns, traffic-lists, hotspot, vpn site-to-site, vpn remote-access, vpn clients, vpn peers, vpn settings patch, and devices port-set (JSONC for switch port config-as-code). Construct the JSON payload, validate it, then apply. See examples/ for payload templates.
unifly networks create -F examples/network-iot-vlan.json
unifly firewall policies create -F examples/firewall-block-iot.jsonReal-time event streaming
# All events
unifly events watch
# Filter by EventCategory (case-insensitive): Device, Client, Network,
# System, Admin, Firewall, Vpn, Unknown
unifly events watch --types "Firewall,Admin"
# JSON stream for piping into alerting
unifly events watch --types Client -o json | jq -c 'select(.severity == "warning")'Firewall policy reorder (round-trippable)
# Read current order for a zone pair
unifly firewall policies reorder --source-zone <zid> --dest-zone <zid> --get
# Write back an explicit order
unifly firewall policies reorder --source-zone <zid> --dest-zone <zid> \
--set "<id1>,<id2>,<id3>"Raw API escape hatch
For endpoints unifly does not wrap (including UniFi v2 routes and Integration paths), use unifly api. It routes through the Session client, so CSRF token management and session caching are automatic.
unifly api "v2/api/site/default/traffic-flow-latest-statistics"
unifly api "cmd/stamgr" -m post -d '{"cmd":"kick-sta","mac":"aa:bb:cc:dd:ee:ff"}'
unifly api "api/s/default/set/setting/teleport" -m put -d '{"enabled":true}'Session API VPN payloads and settings
unifly vpn site-to-site wraps Session API rest/networkconf records whose purpose is site-vpn. This is the current CRUD path for manual IPsec and OpenVPN site-to-site records exposed by the controller.
unifly vpn site-to-site list -o json
unifly vpn site-to-site get <id> -o json
unifly vpn site-to-site create -F site-to-site.json
unifly vpn site-to-site update <id> -F site-to-site.json
unifly vpn site-to-site delete <id>unifly vpn remote-access wraps Session API rest/networkconf records whose purpose is remote-user-vpn. This is the current CRUD path for L2TP, OpenVPN, and WireGuard remote-access servers exposed by the controller.
unifly vpn remote-access list -o json
unifly vpn remote-access get <id> -o json
unifly vpn remote-access create -F remote-access.json
unifly vpn remote-access update <id> -F remote-access.json
unifly vpn remote-access suggest-port -o json
unifly vpn remote-access download-config <id> --path .
unifly vpn remote-access delete <id>unifly vpn clients wraps Session API rest/networkconf records whose purpose is vpn-client. This is the current CRUD path for configured OpenVPN and WireGuard client profiles exposed by the controller.
unifly vpn clients list -o json
unifly vpn clients get <id> -o json
unifly vpn clients create -F vpn-client.json
unifly vpn clients update <id> -F vpn-client.json
unifly vpn clients delete <id>unifly vpn peers wraps the Session v2 API WireGuard peer endpoints for remote-access VPN servers. list can enumerate all peers or scope to a single server ID; create, update, and delete require the parent remote-access server ID.
unifly vpn peers list -o json
unifly vpn peers list <server-id> -o json
unifly vpn peers get <server-id> <peer-id> -o json
unifly vpn peers create <server-id> -F peer.json
unifly vpn peers update <server-id> <peer-id> -F peer.json
unifly vpn peers delete <server-id> <peer-id>
unifly vpn peers subnets -o jsonunifly vpn connections wraps the Session v2 API VPN client connection inventory exposed at v2/api/site/<site>/vpn/connections. restart issues the same controller action the web UI uses for a single connection.
unifly vpn connections list -o json
unifly vpn connections get <id> -o json
unifly vpn connections restart <id>unifly vpn magic-site-to-site wraps the Session v2 API magicsitetositevpn/configs inventory endpoint. It is currently read-only.
unifly vpn magic-site-to-site list -o json
unifly vpn magic-site-to-site get <id> -o jsonunifly vpn settings wraps the Session API rest/setting records for the VPN feature toggles the controller exposes today: teleport, magic-site-to-site-vpn, openvpn, and peer-to-peer.
unifly vpn settings list -o json
unifly vpn settings get peer-to-peer -o json
unifly vpn settings set teleport --enabled true
unifly vpn settings patch peer-to-peer -F peer-to-peer.jsonsite-to-site get, remote-access get, clients get, connections get, peers get, and magic-site-to-site get return redacted records with summary fields and the sanitized controller payload under fields.
settings get returns a redacted wrapper with key, enabled, and fields. patch accepts either the raw session setting body or that wrapper shape and will send the inner fields object back to the controller.
Bulk operations via filter DSL
hotspot purge --filter accepts the Integration filter DSL for bulk deletion without ID iteration:
unifly hotspot purge --filter "status.eq('UNUSED')"
unifly hotspot purge --filter "name.contains('Conference')"TUI handoff for human verification
Propose a change, let a human visually confirm in the TUI before committing:
# Agent inspects, proposes. Human runs unifly tui and verifies on
# screen 4 (Networks) or 5 (Firewall) before the agent applies the change.
unifly tuiMulti-profile targeting
unifly -p home devices list
unifly -p office firewall policies list
UNIFI_PROFILE=warehouse unifly system healthEssential Gotchas
1. Default list limit is 25. The CLI prints a truncation hint when results hit the default. For enumeration, always pass --all or --limit 200 (or higher). 2. Environment variables use the `UNIFI_` prefix, not `UNIFLY_`. Relevant vars: UNIFI_URL, UNIFI_API_KEY, UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_SITE, UNIFI_PROFILE, UNIFI_OUTPUT, UNIFI_INSECURE, UNIFI_TIMEOUT, UNIFI_TOTP. The only UNIFLY_* var is UNIFLY_THEME for the TUI. 3. `--yes` / `-y` skips confirmation prompts for mutations. Required for non-interactive use. 4. API key mode covers most commands on UniFi OS, including Session API endpoints (stats, device commands, Wi-Fi observability, client enrichment). Use Hybrid only when live WebSocket streaming is needed (events watch, TUI live refresh). Client and device enrichment fields work in API key mode. 5. Cloud support is Integration-only. unifly cloud ... talks to Site Manager and auth_mode = "cloud" routes Integration-backed commands through the connector, but Session-only features still need direct controller access. 6. Exit codes are meaningful. 0 on success, non-zero on error. Capture stderr for diagnostics.
Agent Workflow
1. Verify the tool exists with command -v unifly. 2. Check auth mode with unifly config show before running commands that require Session or Integration specifically. 3. Run unifly system health -o json as the first touch to confirm connectivity. 4. Inspect before mutating: list / get the entity first, capture IDs. 5. For complex creates, write a JSON payload and use --from-file. 6. After mutations, re-fetch the entity with get to confirm state. 7. For irreversible operations (delete, reboot, poweroff), surface a summary to the user before running even with --yes.
Additional Resources
Reference Files
- `references/commands.md`: Per-command flag reference with gotchas
(non-obvious flags, dual-API boundaries, correct argument forms)
- `references/concepts.md`: UniFi networking concepts, dual-API gate
matrix, auth decision tree, environment variables, platform config paths, MFA/TOTP, error taxonomy
- `references/workflows.md`: Runnable automation recipes (event
streaming, safe firewall reorder, bulk DHCP reservations, ad-blocking via DNS policies, cafe voucher flow, incident response)
Example Files
- `examples/config.toml`: Multi-profile config template
- `examples/network-iot-vlan.json`: VLAN creation payload for
--from-file - `examples/firewall-block-iot.json`: Firewall policy payload
- `examples/nat-masquerade.json`: NAT masquerade policy payload
- `examples/nat-port-forward.json`: Destination NAT (port forward) payload
- `examples/wifi-iot.json`: WiFi SSID payload
- `examples/vpn-remote-access-wireguard.json`: WireGuard remote-access VPN payload
- `examples/vpn-site-to-site-ipsec.json`: IPsec site-to-site tunnel payload
- `examples/vpn-client-openvpn.json`: OpenVPN client payload
- `examples/vpn-wireguard-peer.json`: WireGuard peer configuration payload
# unifly configuration example
# Location: ~/.config/unifly/config.toml
# Default profile to use when --profile is not specified
default_profile = "home"
# Home controller: UDM Pro with hybrid auth
[profiles.home]
controller = "https://192.168.1.1"
site = "default"
auth_mode = "hybrid"
api_key = "your-integration-api-key-here"
# api_key_env = "UNIFI_HOME_API_KEY" # Alternative: read from env var
username = "admin"
# password stored in OS keyring via: unifly config set-password home
insecure = false
timeout = 30
# Office controller: Cloud Key Gen2 with API key only
[profiles.office]
controller = "https://10.0.0.1"
site = "default"
auth_mode = "integration"
api_key_env = "UNIFI_OFFICE_API_KEY"
insecure = true # self-signed cert
timeout = 60
# Remote site: Session auth only (older firmware or controllers
# without an Integration API key). `"legacy"` is a deprecated alias.
[profiles.warehouse]
controller = "https://warehouse.example.com:8443"
site = "default"
auth_mode = "session"
username = "readonly-admin"
# password stored in keyring; an explicit `password = "..."` here would
# override the keyring entry.
ca_cert = "/etc/ssl/certs/warehouse-ca.pem"
timeout = 45
{
"name": "Block IoT to Internal",
"action": "block",
"source_zone_id": "REPLACE_WITH_IOT_ZONE_UUID",
"destination_zone_id": "REPLACE_WITH_INTERNAL_ZONE_UUID",
"enabled": true,
"logging_enabled": true,
"allow_return_traffic": false,
"description": "Contain IoT devices to their own zone",
"ip_version": "IPV4_ONLY",
"connection_states": ["NEW", "ESTABLISHED"],
"source_filter": {
"type": "network",
"network_ids": ["REPLACE_WITH_IOT_NETWORK_UUID"],
"match_opposite": false
},
"destination_filter": {
"type": "ip_address",
"addresses": ["10.0.0.0/8", "192.168.0.0/16"],
"match_opposite": false
}
}
{
"name": "Masquerade IoT to WAN",
"type": "masquerade",
"description": "Source NAT for IoT VLAN outbound internet traffic",
"enabled": true,
"protocol": "all",
"src_address": "10.0.30.0/24"
}
{
"name": "HTTPS to webserver",
"type": "destination",
"description": "Forward WAN TCP 443 to internal webserver",
"enabled": true,
"protocol": "tcp",
"dst_port": "443",
"translated_address": "10.0.10.50",
"translated_port": "8443"
}
{
"name": "IoT",
"vlan_id": 30,
"subnet": "10.0.30.1/24",
"management": "Gateway",
"purpose": "Corporate",
"dhcp_enabled": true,
"enabled": true,
"dhcp_range_start": "10.0.30.100",
"dhcp_range_stop": "10.0.30.254",
"dhcp_lease_time": 86400,
"dns_servers": ["1.1.1.1", "1.0.0.1"],
"isolation_enabled": true,
"internet_access_enabled": true
}
// Example for `unifly devices port-set <DEVICE> -F switch-ports.jsonc`.
// Splice semantics: ports not listed below are left untouched on the
// device. Per-port `"reset": true` removes that port's override entry
// and returns it to controller defaults.
{
"ports": [
// Trunk uplink with all VLANs (the controller's tagged_vlan_mgmt=auto).
{
"index": 1,
"name": "uplink",
"mode": "trunk",
"native_vlan": "infra",
"tagged_all": true,
"poe": "off",
},
// Access port pinned to a specific VLAN, PoE auto-negotiated.
{
"index": 9,
"name": "mac-mini",
"mode": "access",
"native_vlan": "personal",
"poe": "auto",
},
// Restricted trunk — explicit list of tagged networks. Empty
// `tagged_vlans` would clear the list (JSON Merge Patch); omitting
// the field leaves it untouched.
{
"index": 12,
"name": "lab-switch",
"mode": "trunk",
"native_vlan": "infra",
"tagged_vlans": ["personal", "iot"],
},
// Drop the override on port 16 — controller defaults take over.
{
"index": 16,
"reset": true,
},
],
}
{
"name": "Upstream VPN Provider",
"vpn_type": "openvpn-client",
"enabled": true,
"x_openvpn_username": "REPLACE_ME",
"x_openvpn_password": "REPLACE_ME",
"openvpn_remote_host": "vpn.example.com",
"openvpn_remote_port": 1194,
"openvpn_mode": "udp",
"openvpn_configuration_file": "REPLACE_WITH_OVPN_CONFIG"
}
{
"name": "WireGuard Remote Access",
"vpn_type": "wireguard",
"enabled": true,
"ip_subnet": "10.255.0.1/24",
"wireguard_interface": "wg0",
"local_port": 51820,
"x_wireguard_private_key": "REPLACE_ME",
"dhcpd_dns_enabled": true,
"dhcpd_dns_1": "1.1.1.1",
"dhcpd_dns_2": "1.0.0.1",
"exposed_to_site_vpn": false
}
{
"name": "Branch Office Tunnel",
"vpn_type": "ipsec-vpn",
"enabled": true,
"x_ipsec_pre_shared_key": "REPLACE_ME",
"ipsec_peer_ip": "203.0.113.42",
"ipsec_key_exchange": "ikev2",
"ipsec_ike_encryption": "aes256",
"ipsec_ike_hash": "sha256",
"ipsec_ike_dh_group": 14,
"ipsec_esp_encryption": "aes256",
"ipsec_esp_hash": "sha256",
"ipsec_pfs": true,
"ipsec_ike_lifetime": 28800,
"ipsec_esp_lifetime": 3600,
"remote_vpn_subnets": ["10.20.0.0/24", "10.20.1.0/24"],
"route_distance": 30
}
{
"name": "Bliss Laptop",
"interface_ip": "10.255.0.2",
"public_key": "REPLACE_WITH_CLIENT_PUBLIC_KEY",
"allowed_ips": ["10.255.0.2/32"],
"preshared_key": ""
}
{
"name": "IoT-WiFi",
"ssid": "IoT-WiFi",
"security_mode": "Wpa2Personal",
"passphrase": "IoTSecure2024!",
"enabled": true,
"network_id": "REPLACE_WITH_IOT_NETWORK_UUID",
"hide_ssid": false,
"broadcast_type": "iot-optimized",
"frequencies_ghz": [2.4],
"band_steering": false,
"fast_roaming": false
}
unifly Command Reference
This file is a gotchas-focused reference. Every command accepts --help at runtime with exhaustive flag listings; consult this file for non-obvious flags, dual-API boundaries, correct argument forms, and the cross-cutting patterns listed at the end.
API legend: I = Integration API required. L = Session API required (username + password). H = Works in any mode, but enriched by Hybrid. Consult concepts.md for the full gate matrix.
Global Flags
-p, --profile <NAME> Profile to use
-c, --controller <URL> Override controller URL
-s, --site <SITE> Target site (name or UUID)
-o, --output <FORMAT> table | json | json-compact | yaml | plain
-k, --insecure Accept self-signed TLS
-v, -vv, -vvv Verbose logging
-q, --quiet Suppress non-error output
-y, --yes Skip confirmation prompts
--timeout <SECS> Request timeout (default 30)
--color <MODE> auto | always | never
--no-cache Force fresh login (bypass session cache)
--api-key <KEY> One-shot Integration API key overrideAll also accept the matching UNIFI_* environment variable (see concepts.md).
Hidden but useful for cloud mode:
--host-id <ID>overrides the Site Manager console/host ID used for
connector-routed Integration commands.
Devices [H for list/get, L for commands]
unifly devices list [--all] [-o json]
unifly devices get <id|mac> [-o json]
unifly devices adopt <mac> [--ignore-limit]
unifly devices remove <id|mac>
unifly devices restart <id|mac>
unifly devices locate <mac> [--on true|false]
unifly devices port-cycle <id|mac> <port_idx>
unifly devices ports <id|mac> [--with-clients]
unifly devices ports-export <id|mac> [--all] [--with-clients]
unifly devices port-set <id|mac> [<port_idx>] [--mode access|trunk|mirror]
[--native-vlan <network>] [--tagged-vlans <network,...>]
[--name <label>] [--poe off|auto|pasv24|passthrough]
[--speed auto|10|100|1000|2500|5000|10000]
[-F <FILE> | --from-file <FILE>] [--reset]
unifly devices stats <id|mac>
unifly devices pending
unifly devices upgrade <mac> [--url <firmware-url>]
unifly devices provision <mac>
unifly devices speedtest
unifly devices tags [subcommands]Gotchas:
locate --onis explicit boolean, not a toggle.--on truelights,
--on false clears. Idempotent for automation.
upgrade --urlallows side-loading custom firmware URLs.port-cycleport index is 1-based, matchingdevices ports
output and the controller's wire format. There is no --poe on; use --poe auto (UniFi treats auto as the on/negotiate mode).
ports/port-set/ports-exportuse Session API routes (the
Integration API does not expose port VLAN configuration), but they are reachable using a UniFi OS Integration API key — the session client sends X-API-KEY on UniFi OS session HTTP endpoints, so these work in ApiKey mode on UniFi OS controllers without a username/password. --native-vlan / --tagged-vlans accept network names or session _ids; names are resolved via rest/networkconf, so ambiguous names error out rather than pick one. --mode trunk without --tagged-vlans trunks all VLANs (UniFi's tagged_vlan_mgmt=auto); passing --tagged-vlans switches to custom. port-set preserves every other port's overrides — only the target port's fields are merged.
port-set -F <FILE>applies a JSONC file describing one or more
ports for a single device. Schema: {"ports": [{"index": N, "name": "...", "mode": "trunk", ...}]} with these per-port fields: name, mode, native_network_id (or native_vlan alias), tagged_network_ids (or tagged_vlans), tagged_all, poe, speed, reset. Splice semantics: ports not listed keep their existing override. Per-port "reset": true removes that port's entry from port_overrides (back to controller defaults). Empty tagged_network_ids: [] clears the tagged list (JSON Merge Patch); missing field = no change. PORT_IDX positional becomes optional when -F is set; flag-style --mode/etc. conflict with -F.
ports-export <id|mac>emits the device's current port configuration
as a JSONC file suitable for port-set -F. Sparse by default — only ports with active overrides. Pass --all to include every port (with just index and name for ports that have no override). Round-trip is non-destructive: write the output to a file, then re-apply with port-set <id|mac> -F <file> to restore the same per-port settings.
port-set <SWITCH> <PORT_IDX> --resetremoves that port's
port_overrides entry, returning it to controller defaults. Useful for clearing stale partial overrides (e.g. a port left with native_network_id set but no mode, which the controller then labels mode: "unknown"). Mutually exclusive with the per-field flags (--mode, --native-vlan, etc.) and --from-file. Prompts for confirmation unless -y is set. Equivalent to applying {"ports": [{"index": <PORT_IDX>, "reset": true}]} via -F.
--with-clients(onportsandports-export): annotates each port
with end-user clients and adopted devices (APs, downstream switches) currently observed on it. On ports, adds a connections array in JSON output and a Conns count column (<clients>/<devices>) in the table view. Each entry has a kind discriminator ("client" or "device") plus mac, name, and (for clients) ip / vlan_id. On ports-export, prepends // last-seen <ISO8601>: <mac> (<name>, <kind>) comment lines before each port's {. Stable parse anchor: the literal // last-seen prefix followed by a single space. Markers sort clients before devices then by MAC; one timestamp per export run. Useful for drift detection — re-export and git diff to see when the AP or client on a labelled port changed.
- All device _commands_ (adopt, remove, restart, locate, port-cycle,
port-set, upgrade, provision, speedtest) require Session API access. Only list/get/ports read paths are Hybrid-safe; port-set writes through Session API routes but can be used with UniFi OS API-key auth (per the bullet above), so it is not exclusive to Hybrid/Credentials mode on UniFi OS.
Clients [H for list/find/get, L for roams/wifi + commands/reservations]
unifly clients list [--all] [--type wireless|wired|guest]
unifly clients find <query> # case-insensitive substring over IP, name, hostname, MAC
unifly clients get <mac|id>
unifly clients roams <client> [--limit N] # accepts name, hostname, IP, or MAC
unifly clients wifi <client> # accepts name, hostname, IP, or MAC; aliases: wifi-experience, wifiman
unifly clients authorize <mac> [--minutes N] [--up-rate N] [--down-rate N]
unifly clients unauthorize <mac>
unifly clients block <mac>
unifly clients unblock <mac>
unifly clients kick <mac> # force disconnect
unifly clients forget <mac> # remove from controller memory
unifly clients reservations # alias: res
unifly clients set-ip <mac> --ip <ipv4> [--network <name|id>]
unifly clients remove-ip <mac> [--network <name|id>]Gotchas:
findis the recommended search verb instead oflist | jqpipelines.
It matches substrings across IP, name, hostname, and MAC in a single pass, case-insensitive.
reservations(aliasres) lists all DHCP reservations including
offline clients. It goes through Session API /rest/user.
roamsaccepts any client identifier (name, hostname, IP, or MAC). It
resolves to a MAC via the in-memory client snapshot before hitting GET /v2/api/site/{site}/system-log/client-connection/{mac}. Default limit is 50 events; override with --limit.
roamsoutput includes From/To AP names, SSID, signal strength, channel,
and band for each event. Use it to diagnose sticky clients, ping-pong roaming, and bad handoffs.
set-ipauto-detects the target network from the IP subnet unless
--network is supplied explicitly.
remove-ipdefaults to removing from all networks. Scope it with
--network if the MAC has reservations in multiple networks.
wifiaccepts any client identifier (name, hostname, IP, or MAC). It
resolves to an IP via the in-memory snapshot before hitting GET /v2/api/site/{site}/wifiman/{ip}/. Shows wifi experience score, signal/noise/channel, band, link rates, nearest neighbor APs with signal strength, and the full uplink chain with per-hop experience scores.
wifionly works for wireless clients. Wired clients return a 404.- Band labels are normalized to human-readable:
2.4 GHz,5 GHz,6 GHz. listwireless/bytes/hostname fields are only populated in Hybrid mode.
Networks [I for CRUD]
unifly networks list
unifly networks get <id|name>
unifly networks create --name NAME --vlan N --management MODE \
--ipv4-host <CIDR> [--dhcp --dhcp-start IP --dhcp-stop IP] \
[--dns SERVER]... [-F payload.json]
unifly networks update <id> [flags...]
unifly networks delete <id>
unifly networks refs <id> # reverse referencesGotchas:
- VLAN range is 1-4009 (enforced).
--managementacceptsgateway,switch, orvlan-only.--dnsis repeatable for multiple per-network DNS servers.refsis unique to networks: shows which WiFi SSIDs, firewall policies,
and zones reference a given network. Use before deleting to understand blast radius.
--from-file/-Faccepts a full JSON payload (see examples/).
WiFi [I for CRUD, L for neighbors/channels]
unifly wifi list
unifly wifi get <id|name>
unifly wifi neighbors [--within SECONDS] [--limit N] [--all] # alias: rogueap
unifly wifi channels
unifly wifi create --name SSID --security MODE --passphrase PASS --network ID \
[--broadcast-type standard|iot-optimized] [--frequencies 2.4,5,6] [-F payload.json]
unifly wifi update <id> [flags...]
unifly wifi delete <id>Gotchas:
--securityvalues:open,wpa2-personal,wpa3-personal,
wpa2-wpa3-personal, wpa2-enterprise, wpa3-enterprise.
--broadcast-type iot-optimizedenables IoT optimizations (2.4 GHz-only
limits, lower beacon power).
neighborsusesGET /api/s/{site}/stat/rogueapand surfaces APs seen by
your own radios. Signal is from your AP's perspective, not the neighbor's. Default display limit is 25; use --all or --limit N to see more.
neighbors --withinis in seconds, and the underlyingstat/rogueap
endpoint also uses seconds rather than the epoch milliseconds common in other UniFi stats routes.
channelsusesGET /api/s/{site}/stat/current-channeland shows
regulatory channel availability per band (2.4 GHz, 5 GHz, 5 GHz DFS, 6 GHz) for the controller's country. Useful for DFS planning and verifying which 6 GHz channels are available in your region.
--frequenciesis comma-separated:2.4,5,6. All three are valid
on WiFi 6E and WiFi 7 APs.
--from-fileaccepts full payloads for complex SSID configurations
(enterprise RADIUS, MAC filters, VLAN tagging).
Firewall [I]
Policies
unifly firewall policies list
unifly firewall policies get <id>
unifly firewall policies create --name NAME --action allow|block|reject \
--source-zone ZID --dest-zone ZID \
[--src-ip IP,CIDR,RANGE] [--dst-ip ...] [--src-port N,N] [--dst-port ...] \
[--src-network ID] [--dst-network ID] \
[--src-port-group NAME] [--dst-port-group NAME] \
[--src-address-group NAME] [--dst-address-group NAME] \
[--states NEW,ESTABLISHED] [--ip-version IPV4_ONLY|IPV6_ONLY|BOTH] \
[--description TEXT] [--logging] [--after-system] [-F payload.json]
unifly firewall policies update <id> [flags...]
unifly firewall policies patch <id> [--enabled true|false] [--logging true|false]
unifly firewall policies delete <id>
unifly firewall policies reorder --source-zone ZID --dest-zone ZID (--get | --set "id1,id2,id3") [--after-system]Gotchas:
patchis a fast partial-update for togglingenabled/logging. Use
it instead of update when only changing those fields (cheaper, no round-trip fetch).
--src-ip/--dst-ipaccept a mix of IPs, CIDRs, and ranges
(10.0.0.1-10.0.0.100), comma-separated.
reorder --getprints the current order.reorder --setwrites a new
order. Round-trip pattern: get, edit, set.
--after-systemoncreateplaces the new policy after system-defined
rules in one step (creates then reorders).
reorder --after-systemplaces user policies after system-defined rules.--loggingis a boolean; both bare form (--logging) and explicit
(--logging true) work.
--descriptionexists oncreateandupdate.--from-filesupports shorthand fields:dst_ip,dst_port,src_ip,
src_port, dst_network, src_network, dst_port_group, dst_address_group (and src_* variants). These are resolved into source_filter / destination_filter before submission.
Zones
unifly firewall zones list
unifly firewall zones get <id>
unifly firewall zones create --name NAME [--networks ID,ID,...] [-F payload.json]
unifly firewall zones update <id> [flags...]
unifly firewall zones delete <id>Gotchas:
--networksaccepts comma-separated network IDs or names.--from-fileis now supported on zones (recent addition).
Groups [S]
unifly firewall groups list [--type port-group|address-group|ipv6-address-group] [--all]
unifly firewall groups get <id>
unifly firewall groups create --name NAME [--type port-group|address-group] --members 80,443,8000-8002 [-F payload.json]
unifly firewall groups update <id> [--name NAME] [--members ...] [-F payload.json]
unifly firewall groups delete <id>Gotchas:
- Groups use the Session API (
rest/firewallgroup), not Integration.
Requires ensure_session_access — API key mode on UniFi OS is sufficient.
--typedefaults toport-groupon create. Possible types:
port-group, address-group, ipv6-address-group.
--membersis comma-separated:"80,443,8000-8002"for port groups,
"10.0.30.0/24,10.0.40.1" for address groups.
- The response includes both
_id(Session) andexternal_id(UUID) —
the external_id is what firewall policies reference when using group filters.
- Policies can reference groups by name using
--dst-port-group,
--src-port-group, --dst-address-group, --src-address-group flags or dst_port_group / dst_address_group fields in --from-file JSON. The CLI resolves the name to the group's external_id at create/update time.
NAT [I]
unifly nat policies list
unifly nat policies get <id>
unifly nat policies create --name NAME --nat-type masquerade|source|destination \
[--src-address CIDR] [--dst-address CIDR] \
[--src-port N] [--dst-port N] \
[--translated-address IP] [--translated-port N] \
[--protocol tcp|udp|all] [-F payload.json]
unifly nat policies update <id> [--name NAME | --description DESC] \
[--type masquerade|source|destination] [--protocol tcp|udp|all] \
[--enabled true|false] [--src-address CIDR] [--dst-address CIDR] \
[--src-port N] [--dst-port N] \
[--translated-address IP] [--translated-port N] [-F payload.json]
unifly nat policies delete <id>Gotchas:
- `nat policies update <id>` fetches the existing rule and merges
only the changed fields. Use --name or --description (mutually exclusive) for the display label, plus any combination of --type, --protocol, --enabled, address/port flags, or --from-file.
masqueradeis source NAT using the outgoing interface address (most
common for Internet-bound traffic).
destinationis how port forwarding works on UniFi: specify
--dst-port (the external port), --translated-address (internal IP), and --translated-port (internal port).
--from-fileaccepts full payloads.
ACL [I]
unifly acl list # alias: ls
unifly acl get <id>
unifly acl create [flags...] [-F payload.json]
unifly acl update <id> [flags...] [-F payload.json]
unifly acl delete <id>
unifly acl reorder [--get | --set "id1,id2,id3"]Similar reorder semantics to firewall policies.
DNS [I]
unifly dns list
unifly dns get <id>
unifly dns create --domain NAME --record-type A|AAAA|CNAME|MX|TXT|SRV|Forward \
--value VALUE [--ttl SECS] [-F payload.json]
unifly dns update <id> [flags...]
unifly dns delete <id>Gotchas:
--ttlrange is0-86400(enforced).Forwardrecord type sets up DNS forwarding for a domain.
Traffic Lists [I]
unifly traffic-lists list
unifly traffic-lists get <id>
unifly traffic-lists create --name NAME --list-type ports|ipv4|ipv6 --values "80,443" [-F payload.json]
unifly traffic-lists update <id> [flags...]
unifly traffic-lists delete <id>Gotchas:
--list-typeis required.ports,ipv4, oripv6.- Referenced by firewall policies, NAT policies, and ACLs. Ideal for
avoiding rule duplication.
Hotspot [I]
unifly hotspot list
unifly hotspot get <id>
unifly hotspot create --name NAME --count N --minutes N [--quota MB] [--up-rate KBPS] [--down-rate KBPS]
unifly hotspot delete <id>
unifly hotspot purge --filter "EXPR"Gotchas:
create --count Ngenerates N voucher codes in one call. Each code
inherits the other flags (duration, quota, rate limits).
purge --filteraccepts the Integration filter DSL and is unifly's
only bulk-delete-by-expression operation. Examples: status.eq('UNUSED'), name.contains('Conference'), created_at.lt('2024-01-01'). Use carefully; it deletes matching vouchers immediately.
Events [L]
unifly events list [--within HOURS] [--limit N]
unifly events watch [--types CAT1,CAT2] [-o json]Gotchas:
watch --typesfilter values are EventCategory enum names,
case-insensitive: Device, Client, Network, System, Admin, Firewall, Vpn, Unknown. Comma-separated. *`EVT_` glob patterns do not work**, despite what older documentation may suggest.
watchstreams from WebSocket. It runs until Ctrl-C. Use-o jsonand
pipe into jq -c for line-delimited JSON for downstream processing.
list --within HOURSlimits to the last N hours.
Stats [L]
unifly stats site [--interval 5minute|hourly|daily|monthly] [--start ISO] [--end ISO]
unifly stats device <mac> [--interval ...] [--attrs attr1,attr2]
unifly stats client <mac> [--interval ...]
unifly stats gateway [--interval ...]
unifly stats dpi [--group-by by-app|by-cat] [--macs MAC1,MAC2]Gotchas:
--start/--endare ISO 8601 timestamps (2024-01-01T00:00:00Z).--attrslimits the metrics returned; smaller payloads, faster queries.stats dpirequires--group-by.by-appbuckets by application,
by-cat buckets by category.
- Session API only; all commands fail without credentials.
DPI [I for apps/categories, L for status/enable/disable]
unifly dpi apps
unifly dpi categories
unifly dpi status
unifly dpi enable
unifly dpi disableGotchas:
appsandcategoriesare Integration API reference lookups.status,enable,disableare Session API lifecycle controls for the
DPI subsystem itself. Use these to toggle DPI on/off without touching the web UI.
Settings [L]
unifly settings list
unifly settings get <KEY>
unifly settings set <KEY> <FIELD> <VALUE>
unifly settings set <KEY> --data '{"field": "value"}'
unifly settings exportGotchas:
- All subcommands use the Session API (
rest/setting/set/setting/{key}). listshows a summary table of all ~44 setting sections with key, field
count, enabled status, and notable values.
getmasksx_-prefixed fields (credentials, internal secrets) in table
mode. Use -o json to see everything.
setperforms a read-modify-write: fetches the current section, patches
the specified field, and PUTs the full section back. Values are parsed as bool (true/false), number, or string fallback.
set --datamerges a JSON object into the section. Mutually exclusive
with the positional <FIELD> <VALUE> form.
exportalways outputs full JSON regardless of--outputflag.- The PUT endpoint replaces the entire section; the handler strips
_id,
site_id, and key metadata before sending.
System [L]
unifly system info
unifly system health
unifly system sysinfo
unifly system backup create
unifly system backup list
unifly system backup download <filename> [--path DIR]
unifly system backup delete <filename>
unifly system reboot
unifly system poweroffGotchas:
backup download --path DIRwrites to a specific directory instead of
cwd.
backup deleteis scoped to a specific backup file.rebootandpoweroffare destructive. Always summarize to the user
before running even with --yes.
Admin [L]
unifly admin list
unifly admin invite --email EMAIL --role ROLE
unifly admin revoke <admin_id>
unifly admin update <admin_id> [--role ROLE]Gotchas:
revokeandupdatetake a positional `<admin_id>`, not
--email. Pre-fetch the ID via admin list -o json before revoking.
Sites [L]
unifly sites list
unifly sites create --name NAME --description TEXT
unifly sites delete <name>Site create --description is required.
VPN [I for servers/tunnels, L for site-to-site/remote-access/clients/peers/settings, V2 for connections/magic-site-to-site]
unifly vpn servers [--all] [-o json]
unifly vpn tunnels [--all] [-o json]
unifly vpn site-to-site list [--all] [-o json]
unifly vpn site-to-site get <id> [-o json]
unifly vpn site-to-site create -F payload.json
unifly vpn site-to-site update <id> -F payload.json
unifly vpn site-to-site delete <id>
unifly vpn remote-access list [--all] [-o json]
unifly vpn remote-access get <id> [-o json]
unifly vpn remote-access create -F payload.json
unifly vpn remote-access update <id> -F payload.json
unifly vpn remote-access suggest-port [-o json]
unifly vpn remote-access download-config <id> [--path PATH]
unifly vpn remote-access delete <id>
unifly vpn clients list [--all] [-o json]
unifly vpn clients get <id> [-o json]
unifly vpn clients create -F payload.json
unifly vpn clients update <id> -F payload.json
unifly vpn clients delete <id>
unifly vpn connections list [--all] [-o json]
unifly vpn connections get <id> [-o json]
unifly vpn connections restart <id>
unifly vpn peers list [server-id] [--all] [-o json]
unifly vpn peers get <server-id> <id> [-o json]
unifly vpn peers create <server-id> -F payload.json
unifly vpn peers update <server-id> <id> -F payload.json
unifly vpn peers delete <server-id> <id>
unifly vpn peers subnets [-o json]
unifly vpn magic-site-to-site list [--all] [-o json]
unifly vpn magic-site-to-site get <id> [-o json]
unifly vpn settings list [--all] [-o json]
unifly vpn settings get <teleport|magic-site-to-site-vpn|openvpn|peer-to-peer> [-o json]
unifly vpn settings set <key> --enabled true|false
unifly vpn settings patch <key> -F payload.jsonGotchas:
serversandtunnelsare Integration API inventory only.site-to-siteuses Session APIrest/networkconfrecords filtered to
purpose=site-vpn.
remote-accessuses Session APIrest/networkconfrecords filtered to
purpose=remote-user-vpn.
remote-access suggest-portuses the Session v2 API
network/port-suggest?service=openvpn helper and returns available_ports.
remote-access download-configfetches the Session v2 API
vpn/openvpn/<id>/configuration export and writes <id>.ovpn by default.
clientsuses Session APIrest/networkconfrecords filtered to
purpose=vpn-client.
connectionsuses the Session v2 APIvpn/connectionsinventory and
vpn/<id>/restart action.
peersuses Session v2 APIwireguard/*/usersbatch endpoints.create,
update, and delete require the parent remote-access server ID.
subnetslists already-consumed subnets from
v2/api/site/<site>/wireguard/users/existing-subnets.
magic-site-to-siteuses the Session v2 API
magicsitetositevpn/configs inventory endpoint and is read-only.
settingsuses Session APIrest/settingrecords for VPN feature toggles.patchaccepts either a raw session setting body or the wrapper emitted
by get ({ "key": ..., "enabled": ..., "fields": { ... } }).
- Sensitive nested material such as private keys and PSKs is redacted from
get output. Reconstruct those fields explicitly before updating if the controller requires them unchanged.
Alarms [L]
unifly alarms list [--unarchived]
unifly alarms archive <id>
unifly alarms archive-allAPI (raw passthrough) [any mode]
unifly api <path> [-m get|post|put|patch|delete] [-d '<json-body>']Gotchas:
- Routes through the Session client, so CSRF tokens and session caching
are handled automatically.
- Paths are relative to the controller base URL. Examples:
- Session v1:
api/s/default/stat/device - Session v2:
v2/api/site/default/traffic-flow-latest-statistics - Integration v1:
integration/v1/sites/default/clients - Commands:
cmd/stamgr,cmd/devmgr -d '<json>'is used forpost,put, andpatch.deletedoes not require a body.- Essential when unifly does not wrap a specific endpoint yet.
Cloud [cloud fleet API]
unifly cloud hosts
unifly cloud hosts get <id>
unifly cloud sites
unifly cloud switch <site>
unifly cloud devices [--host <id>]...
unifly cloud isp [--type 5m|1h]
unifly cloud isp query --sites <site-1,site-2>
unifly cloud sdwan
unifly cloud sdwan get <id>
unifly cloud sdwan status <id>Gotchas:
unifly cloud ...talks directly toapi.ui.com/v1/; it does not
create a Controller or use the local Session API.
cloud devices --hostis repeatable. Omit it to list devices across all
accessible consoles.
cloud isp query --sitesis comma-delimited and returns a warning if Site
Manager responds with partialSuccess.
cloud switch <site>updates the active cloud profile'ssitefield using
the controller connector's site inventory. It accepts a site name, internal reference, or UUID, and stores the controller-side internal reference.
cloudcommands only need a Site Manager API key. They do not need
host_id.
- Regular commands such as
networks listorfirewall policies liststill
need host_id in cloud mode, but unifly will auto-resolve it when the API key only exposes one console, or one owner console.
Topology, TUI, Completions, Config, Countries
unifly topology: Pretty-print the gateway > switch > AP > client tree
(Hybrid recommended for complete uplink data).
unifly tui [--theme NAME] [--log-file PATH]: Launches the Ratatui
dashboard. UNIFLY_THEME env var also sets the theme.
unifly completions bash|zsh|fish|powershell|elvish: Emit completion
script to stdout.
unifly config init | cloud-setup | show | set | profiles | use | set-password:
Profile management. cloud-setup validates a Site Manager API key, lets you pick a console and site interactively, and writes a cloud profile. set-password stores secrets in the OS keyring.
unifly countries: List country codes for WiFi regulatory settings.
Cross-Cutting Patterns
--from-file / -F (universal create/update)
Accepted by: networks, wifi, firewall policies, firewall zones, firewall groups, nat policies, acl, dns, traffic-lists, hotspot, vpn site-to-site, vpn remote-access, vpn clients, vpn peers, vpn settings patch, and devices port-set (JSONC for switch port config-as-code). The flag mutually excludes inline flags on the same field. Prefer --from-file for anything beyond a handful of flags.
unifly networks create -F network.json
unifly firewall policies create -F policy.jsonSee examples/ for payload templates.
Integration Filter DSL
--filter on list commands and hotspot purge --filter accepts a small expression language:
field.eq('value')
field.neq('value')
field.contains('substring')
field.startswith('prefix')
field.endswith('suffix')
field.gt(123), field.lt(123), field.gte, field.lte
field.in(['a', 'b', 'c'])Combine with && and ||:
unifly devices list --filter "state.eq('ONLINE') && model.startswith('U6')"Only Integration API commands respect --filter. Session commands filter client-side via jq after fetching.
Default List Limit Is 25
All list commands default to --limit 25 and print a truncation hint when results hit the ceiling. For enumeration use --all (auto-paginate) or --limit 200 (or higher) explicitly. Agents running enumeration queries should always pass one of these flags to avoid silent truncation.
Output Modes for Pipelines
-o json: Structured output, the default for agent use-o json-compact: Single-line JSON per record, great for line-oriented
processing
-o plain: Emits IDs one per line, ideal forxargs:
unifly clients list -o plain | xargs -n1 unifly clients block-o table: Human display only, not for parsing
Dry-Run-Like Patterns
unifly does not have an explicit --dry-run flag. The idiomatic patterns are:
1. Read before write. get the entity, show it to the user, then update. 2. Use `reorder --get` for firewall/ACL ordering changes before reorder --set. 3. Use `networks refs <id>` before deleting a network to see what depends on it. 4. Hand off to the TUI for visual verification on the Firewall, Networks, or Devices screens.
UniFi Networking Concepts
Reference for UniFi networking concepts and unifly-specific operational details. Consult this file when the user's task requires decisions about authentication mode, troubleshooting auth errors, or understanding the dual-API architecture.
Architecture
Controller
The UniFi controller (UniFi OS Console) is the central management platform for UniFi network devices. It runs on dedicated hardware (UDM, UDM Pro, UCG, UXG) or as a self-hosted application (UniFi Network application for Linux). unifly communicates with the controller via its REST APIs and WebSocket events channel.
Sites
A controller can manage multiple sites, which are logical groupings of devices and configuration. Each site has its own networks, firewall rules, WiFi SSIDs, and client database. Most unifly commands operate within a single site context (set via --site, UNIFI_SITE, or the config profile). The default site is named default.
Device Types
| Prefix | Type | Examples |
|---|---|---|
| UDM | Dream Machine | UDM, UDM Pro, UDM SE |
| UCG | Cloud Gateway | UCG Ultra, UCG Max |
| UXG | Cloud Gateway (newer) | UXG Pro, UXG Lite |
| USG | Security Gateway | USG, USG Pro (legacy) |
| USW | Switch | USW Lite 8, USW Pro 24 PoE |
| UAP | Access Point (legacy) | UAP-AC-Lite, UAP-AC-HD |
| U6 | WiFi 6 Access Point | U6-Lite, U6-Pro, U6-Enterprise |
| U7 | WiFi 7 Access Point | U7-Pro, U7-Pro-Max |
| UNVR | Network Video Recorder | UNVR, UNVR Pro |
| UXBG | Building Bridge | UXBG Pro |
Device States
- ONLINE: Device is connected and operating normally
- OFFLINE: Device is unreachable
- PENDING: Device discovered but not yet adopted
- ADOPTING: Adoption in progress
- UPGRADING: Firmware upgrade in progress
- PROVISIONING: Configuration being applied
Dual-API Architecture
unifly is unusual among UniFi tools because it speaks both the modern Integration API and the older Session API, reconciling data between them where necessary. Understanding which API handles which operation is the most important operational knowledge for agents.
Integration API
- Auth: API key in
X-API-KEYheader - Base path:
/proxy/network/integration/v1/ - Format: Modern JSON with UUIDs
- Returns: Clean, well-typed responses without envelope wrapping
- Limitation: Missing several device-level operations, historical stats,
WebSocket events, and some client enrichment fields (bytes, hostname, uplink MAC, VLAN, wireless)
- Best for: Configuration CRUD (networks, WiFi, firewall, NAT, DNS, ACL,
traffic lists, hotspot, WANs, RADIUS)
Session API
- Auth: Session cookie plus CSRF token for session login. On UniFi OS,
session HTTP endpoints also accept X-API-KEY; session WebSocket does not.
- Base path:
/proxy/network/api/and/proxy/network/v2/api/ - Format: Envelope-wrapped JSON (
{"meta": {...}, "data": [...]}) - Returns: Everything the controller web UI sees, including fields the
Integration API omits
- Best for: Events (WebSocket), stats, device commands (restart, adopt,
locate, speedtest, port-cycle, upgrade, provision), DPI lifecycle, admin management, backups, site management
Hybrid Mode
Hybrid merges both clients at login time. On every devices list or clients list, unifly fetches the Integration API first, then supplements each record with Session fields: tx_bytes/rx_bytes, hostname, wireless, uplink_device_mac, vlan, client_count (mapped from Session API num_sta). The same merge also works in API key mode on UniFi OS because the controller accepts X-API-KEY on session HTTP routes.
Hybrid is still the safest default when you need live WebSocket features (events watch) or maximum compatibility across controller variants.
Command Authentication Gate Matrix
Only Integration-only commands call ensure_integration_access. Session-backed commands fail naturally when the session client is unavailable. On UniFi OS, API key mode instantiates both the Integration client and a session HTTP client, so most HTTP commands work without username/password. Use this matrix to pick the right auth_mode.
Integration API required (API key)
acl(list/get/create/update/delete/reorder)dns(list/get/create/update/delete)firewall policies(all subcommands)firewall zones(all subcommands)hotspot(list/get/create/delete/purge)nat policies(list/get/create/delete)networks(list/get/create/update/delete/refs)traffic-lists(list/get/create/update/delete)wans(list)wifi(list/get/create/update/delete)countriesradius profilesvpn servers(list/get)vpn tunnels(list/get)
Session HTTP-backed (username + password, or API key on UniFi OS)
firewall groups(list/get/create/update/delete):/rest/firewallgroupadmin(list/invite/revoke/update):/rest/adminalarms(list/archive/archive-all)clients reservations,clients set-ip,clients remove-ip:/rest/userclients roams <mac>:/v2/api/site/{site}/system-log/client-connection/{mac}clients wifi <ip>:/v2/api/site/{site}/wifiman/{ip}/devicesadopt, remove, restart, locate, port-cycle, upgrade, provision,
speedtest (all route through cmd/devmgr and cmd/stamgr)
clientsauthorize, unauthorize, block, unblock, kick, forget (via
cmd/stamgr)
dpi status | enable | disable:/set/setting/dpievents list:/stat/eventsites create | deletestats site | device | client | gateway | dpi:/stat/report/*system health | sysinfo | backup | reboot | poweroffvpn site-to-site(list/get/create/update/delete):/rest/networkconfvpn remote-access(list/get/create/update/delete/suggest-port/download-config):/rest/networkconf,/v2/api/site/{site}/network/port-suggest,/v2/api/site/{site}/vpn/openvpn/{id}/configurationvpn clients(list/get/create/update/delete):/rest/networkconfvpn connections(list/get/restart):/v2/api/site/{site}/vpn/connectionsvpn peers(list/get/create/update/delete/subnets):/v2/api/site/{site}/wireguard/*/usersvpn magic-site-to-site(list/get):/v2/api/site/{site}/magicsitetositevpn/configsvpn settings(list/get/set/patch):/rest/settingwifi neighbors:/stat/rogueapwifi channels:/stat/current-channelnat policies(list/get/create/update/delete): Session v2 API
Session WebSocket required (session-backed auth)
events watch
The TUI can still launch without WebSocket auth, but live event streaming falls back to polling when no session cookie is available.
Session HTTP for VPN observability
vpn status: IPsec SA state via/stat/ipsec-savpn health: VPN subsystem health from cached store (populated by session refresh)
Enriched when session HTTP is available
clients list: Integration fetch, Session fields merged by IP matchclients find: inherits the merged viewdevices list: Integration fetch, Session APInum_stamerged by MACtopology: depends on mergeduplink_device_macfor tree construction
Raw API escape hatch
api <path>: Routes through the Session client and handles auth
automatically (API key on UniFi OS, or CSRF/session for session login). Can reach Session, v2 (v2/api/site/...), and Integration (integration/v1/...) endpoints regardless of auth mode.
Session API Endpoint Quirks
- `stat/rogueap` uses epoch seconds, not milliseconds. The
--within
filter on wifi neighbors maps to this behavior directly, so passing millisecond-style values silently returns empty data.
- `wifiman/{ip}/` band codes differ from `stat/sta`. The Wi-Fi
experience endpoint uses 2.4g / 5g / 6g, while station data often uses ng / na / 6e.
- *`stat/report/.ap
and.site` use different attribute prefixes*.
.ap expects bare fields like ng-cu_total, while .site uses prefixed names such as ap-ng-cu_total.
- **
system-log/client-connection/{mac}duplicates the MAC in query
params**. Include the client MAC in both the path and ?mac= or the endpoint may return empty results.
- v2 endpoints return raw JSON, not the
{meta, data}envelope. Use
get_raw() / raw_get() patterns for clients roams, clients wifi, and similar observability routes.
Auth Mode Decision Tree
1. "I only have an API key" → auth_mode = "integration". On UniFi OS, most HTTP commands work, including stats, device commands, reservations, Wi-Fi observability (wifi neighbors, wifi channels, clients roams, clients wifi), admin operations, and enriched clients list / devices list. Live events watch still will not. 2. "I have username + password only" → auth_mode = "session". Events, stats, device commands work. Modern entities (DNS policies, NAT policies, traffic lists, ACL) require Integration and will fail. 3. "I have both" → auth_mode = "hybrid". Recommended when the task needs live WebSocket streaming or maximum controller compatibility. 4. "Agent will manage multiple sites/controllers" → Use named profiles (-p home, -p office) with Hybrid on each. Credentials go in the OS keyring via unifly config set-password --profile <name>.
Configuration
Platform-Native Config Paths
| OS | Config File |
|---|---|
| Linux | ~/.config/unifly/config.toml |
| macOS | ~/.config/unifly/config.toml |
| Windows | %APPDATA%\unifly\config.toml |
Unix platforms (Linux and macOS) use XDG-standard paths. Windows uses platform-native %APPDATA%. Agents should not assume Unix paths on Windows.
Environment Variables
Agent operations should prefer the UNIFI_ prefix environment variables over CLI flags when running in automation contexts:
| Variable | Purpose |
|---|---|
UNIFI_URL | Controller URL (overrides profile) |
UNIFI_API_KEY | Integration API key |
UNIFI_USERNAME | Session API username |
UNIFI_PASSWORD | Session API password (prefer keyring in interactive) |
UNIFI_SITE | Target site name or UUID |
UNIFI_PROFILE | Active profile |
UNIFI_OUTPUT | Default output format |
UNIFI_INSECURE | 1 to accept self-signed TLS certs |
UNIFI_TIMEOUT | Request timeout in seconds |
UNIFI_TOTP | One-time password for MFA-protected accounts |
NO_COLOR | Standard no-color flag (respected by output painter) |
UNIFLY_THEME | TUI theme name (TUI only, not CLI) |
Resolution priority (highest wins): CLI flags > environment variables > config file > built-in defaults.
MFA / TOTP
If the controller requires TOTP two-factor auth, set a totp source:
[profiles.home]
controller = "https://192.168.1.1"
auth_mode = "hybrid"
totp_env = "UNIFI_TOTP"Supply the current code via the env var at runtime:
UNIFI_TOTP=$(op read "op://Personal/UniFi/one-time password") \
unifly devices listThe --totp CLI flag also exists but is hidden from --help. Use it for one-shot operations.
Session Cache
unifly caches the session cookie across commands for speed. To force a fresh login (e.g. after password rotation):
unifly --no-cache devices listNetworking Primitives
VLANs
Virtual LANs segment the network at Layer 2. UniFi networks accept a VLAN ID in the range 1-4009 (unifly enforces this). The default network typically uses VLAN 1 or is untagged.
Common VLAN design:
| VLAN | Name | Subnet | Purpose |
|---|---|---|---|
| 1 | Default | 192.168.1.0/24 | Management |
| 10 | Trusted | 10.0.10.0/24 | Trusted devices |
| 20 | Guest | 10.0.20.0/24 | Guest isolation |
| 30 | IoT | 10.0.30.0/24 | IoT devices |
| 40 | Cameras | 10.0.40.0/24 | Surveillance |
Network Management Types
- Gateway: Routed network with DHCP, NAT, firewall (most common)
- Switch: Layer 2 only, no routing
- VLAN-only: Tag without a subnet, used in trunk scenarios
DHCP
UniFi supports three DHCP modes:
- Server: Controller or gateway runs DHCP (most common)
- Relay: Forward DHCP to an upstream server
- None: No DHCP, static IPs only
IPv6
Dual-stack networking supports:
- SLAAC: Stateless Address Autoconfiguration
- DHCPv6: Stateful IPv6 address assignment
- Prefix delegation: Automatic prefix from upstream (PD)
Security Model
Firewall Zones and Policies
UniFi uses a zone-based firewall. Zones group networks, and policies control traffic between zone pairs.
Built-in zones:
- Internal: LAN networks
- External:WAN/Internet traffic
- DMZ: Public-facing services
- VPN: VPN-originated traffic
- Hotspot: Guest/hotspot networks
Policies define rules between source and destination zones:
- Action:
ALLOW,BLOCK,REJECT - Direction: Implied by zone pair
- Logging: Optional rule-level logging
- Order: First match wins; ordering matters
NAT Policies
NAT policies (unifly nat policies) support three kinds:
- Masquerade: Source NAT using the outgoing interface address (most
common for Internet-bound traffic)
- Source NAT: Explicit source address rewrite
- Destination NAT: Port forwarding and DNAT
ACLs
ACLs (unifly acl) provide device-level access control independent of firewall zones, filtering by IP, MAC, port, or protocol.
Traffic Matching Lists
Reusable lists of ports, IPv4 addresses, or IPv6 addresses that firewall policies, NAT policies, and ACLs reference. Reduces duplication across rules.
Events and Monitoring
Event Categories
Events flowing through unifly events watch are tagged with one of these categories (used by --types filter, case-insensitive):
| Category | Examples |
|---|---|
Device | Adoption, restart, firmware update, port flap |
Client | Connect, disconnect, roam, block, unblock, authorize |
Network | Interface state, VLAN changes, DHCP exhaustion |
System | Controller restart, configuration push, backup |
Admin | Login, logout, configuration change |
Firewall | Policy hit (when logging enabled), IDS/IPS alert |
Vpn | Tunnel up/down, client connect |
Unknown | Fallback for uncategorized events |
Alarms
Alarms are persistent alerts that require acknowledgment. Distinct from transient events. List with unifly alarms list, archive individually with unifly alarms archive <id>, or clear everything with unifly alarms archive-all.
Historical Stats
unifly stats pulls from Session API report endpoints. Supported intervals:
5minute: High resolution, short retention windowhourly: Medium resolutiondaily: Long-term trendsmonthly: Capacity planning
Subcommands: site, device, client, gateway, dpi. The dpi subcommand supports --group-by by-app or --group-by by-cat.
DPI (Deep Packet Inspection)
unifly dpi apps: List known applications (Integration API)unifly dpi categories: List known categories (Integration API)unifly dpi status: Current DPI enable state (Session API)unifly dpi enable: Turn DPI on (Session API)unifly dpi disable: Turn DPI off (Session API)unifly stats dpi: Query DPI traffic breakdown
Error Taxonomy
Common failures and how to diagnose them:
| Error | Root Cause | Fix |
|---|---|---|
Unsupported { required: "Integration API" } | Command needs API key, running in session mode | Switch to hybrid or integration mode |
Unsupported { required: "Session API" } | Command needs credentials, running in integration mode | Switch to hybrid or session mode |
403 on POST/PUT/DELETE via /proxy/network/ | Missing CSRF token | Re-login (cache invalidated); --no-cache |
tls error: self-signed certificate | Controller uses self-signed TLS | Use -k/--insecure or UNIFI_INSECURE=1 |
profile 'foo' not found | No matching profile in config | Run unifly config profiles to list |
keyring error | Keyring daemon not running (Linux) | Unlock keyring or use plaintext config |
Integration filter parse error | Bad filter DSL syntax | Check .eq('x'), .contains('y') form |
Empty clients list wireless/bytes fields | Integration-only mode | Switch to Hybrid for enriched fields |
| Silent result truncation (25 rows) | Default list limit | Pass --all or --limit 200 |
Limits and Known Gaps
- Cloud support is Integration-only.
unifly cloud ...talks to the Site
Manager fleet API at api.ui.com/v1/, and auth_mode = "cloud" tunnels Integration-backed commands through the cloud connector. Session-only surfaces such as events watch, Wi-Fi observability, and admin/session workflows still require direct controller access.
- VPN coverage is broad but split across two APIs. Integration API
provides unifly vpn servers (with get/detail), unifly vpn tunnels (with get/detail), unifly vpn status (IPsec SA), and unifly vpn health. Session API provides full CRUD via unifly vpn site-to-site, unifly vpn remote-access (including suggest-port and download-config), unifly vpn clients, unifly vpn peers (WireGuard), unifly vpn connections, and unifly vpn settings. unifly vpn magic-site-to-site is read-only. UniFi-to-UniFi auto flows are still not wrapped.
- Port forwarding lives under
nat policieswith destination NAT, not
a dedicated command.
- `nat policies update` is now available. It fetches the existing
rule and merges only the changed fields via the Session v2 API.
- DeviceFilter lacks a `BySite` variant. Filter client-side after
fetching if cross-site device filtering is required.
Automation Workflows
Runnable automation recipes using unifly. Each recipe is copy-paste-ready. For command-level flag details, consult commands.md. For auth and architecture, consult concepts.md.
Pre-Flight Checks
Before any automation, verify connectivity and the resolved auth mode:
# Tool present and version
unifly --version
# Resolved profile and auth_mode
unifly config show
# Controller reachable, site healthy
unifly system health -o json -qAlways pass -o json for machine-parseable output and -y/--yes to skip confirmation prompts in non-interactive scripts.
Payload-Driven Provisioning with --from-file
Most entities accept --from-file (alias -F) for a complete JSON payload instead of flag salad. This is the preferred pattern for agents managing complex configurations because payloads can be version-controlled, templated, and validated before apply.
# Validate the payload with jq first
jq empty examples/network-iot-vlan.json && echo "Valid JSON"
# Apply
unifly networks create -F examples/network-iot-vlan.json -o json
# For updates, pass the ID positionally
unifly networks update <network-id> -F examples/network-iot-vlan.jsonNetwork Provisioning
Create a Complete VLAN Segment
Network, firewall zone, WiFi SSID, and an isolation policy in one script:
#!/usr/bin/env bash
set -euo pipefail
# 1. Create the network
NETWORK_ID=$(unifly networks create \
--name "IoT" \
--vlan 30 \
--management gateway \
--ipv4-host 10.0.30.1/24 \
--dhcp --dhcp-start 10.0.30.100 --dhcp-stop 10.0.30.254 \
--dns 1.1.1.1 --dns 1.0.0.1 \
-o json | jq -r '.id')
# 2. Create a firewall zone that owns it
ZONE_ID=$(unifly firewall zones create \
--name "IoT Zone" \
--networks "$NETWORK_ID" \
-o json | jq -r '.id')
# 3. Grab the Internal zone ID (default LAN zone)
INTERNAL_ZONE_ID=$(unifly firewall zones list -o json | \
jq -r '.[] | select(.name == "Internal") | .id')
# 4. Block IoT from reaching Internal
unifly firewall policies create \
--name "Block IoT to Internal" \
--action block \
--source-zone "$ZONE_ID" \
--dest-zone "$INTERNAL_ZONE_ID" \
--description "Contain IoT devices to their own zone" \
--logging
# 5. Create an IoT-optimized SSID on the network
unifly wifi create \
--name "IoT-WiFi" \
--broadcast-type iot-optimized \
--security wpa2-personal \
--passphrase "IoTSecure2024!" \
--network "$NETWORK_ID"
echo "IoT VLAN 30 provisioned: network=$NETWORK_ID zone=$ZONE_ID"Bulk DNS Records from CSV
# CSV format: domain,type,value,ttl
while IFS=',' read -r domain type value ttl; do
[ "$domain" = "domain" ] && continue # skip header
unifly dns create \
--domain "$domain" \
--record-type "$type" \
--value "$value" \
--ttl "${ttl:-3600}" \
-o json
done < dns_records.csvBulk DHCP Reservations from JSON
One of the most-requested UniFi automation patterns. Drive reservations from a source of truth (Ansible inventory, Terraform state, spreadsheet export) instead of clicking through the web UI.
#!/usr/bin/env bash
# reservations.json format:
# [
# {"mac": "aa:bb:cc:dd:ee:01", "ip": "10.0.30.10", "network": "IoT", "name": "Printer"},
# {"mac": "aa:bb:cc:dd:ee:02", "ip": "10.0.30.11", "network": "IoT", "name": "NAS"}
# ]
set -euo pipefail
jq -c '.[]' reservations.json | while read -r entry; do
mac=$(echo "$entry" | jq -r '.mac')
ip=$(echo "$entry" | jq -r '.ip')
network=$(echo "$entry" | jq -r '.network')
name=$(echo "$entry" | jq -r '.name // empty')
# Set the IP reservation (creates the client record if needed)
unifly clients set-ip "$mac" --ip "$ip" --network "$network" || {
echo "Failed to reserve $ip for $mac" >&2
continue
}
echo "Reserved $ip -> $mac ($name)"
donePort Forwarding via NAT Policies
Port forwarding is nat policies create --nat-type destination. No dedicated port-forward command exists.
# Forward external TCP 443 -> internal 10.0.10.50:8443
unifly nat policies create \
--name "HTTPS to webserver" \
--nat-type destination \
--protocol tcp \
--dst-port 443 \
--translated-address 10.0.10.50 \
--translated-port 8443To modify a NAT policy, use nat policies update <ID> with any combination of flags. Only the specified fields are changed.
Real-Time Event Streaming
unifly's WebSocket event stream is one of its most differentiating capabilities. Agents can watch events live and react to network changes.
Stream All Events
unifly events watchFilter by Category
The --types flag matches EventCategory enum values case-insensitively. Valid values: Device, Client, Network, System, Admin, Firewall, Vpn, Unknown.
# Watch firewall and admin events only
unifly events watch --types "Firewall,Admin"
# Device events for fleet health monitoring
unifly events watch --types DevicePipe Events Into Alerting
# Forward warning-level client events to a webhook as line-delimited JSON
unifly events watch --types Client -o json | \
jq -c 'select(.severity == "warning" or .severity == "error")' | \
while read -r event; do
curl -fsSL -X POST "$SLACK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"text\": $(echo "$event" | jq '.message')}"
doneIncident Triage Loop
# Watch for firewall events and correlate with the blocked source IP
unifly events watch --types Firewall -o json | while read -r event; do
src=$(echo "$event" | jq -r '.src_ip // empty')
[ -z "$src" ] && continue
echo "Firewall hit from $src"
unifly clients find "$src" -o json | jq '.[] | {name, hostname, mac, vlan}'
doneSafe Firewall Policy Reorder
Firewall policy order matters: first match wins. The reorder --get / reorder --set pattern is round-trippable and safe.
#!/usr/bin/env bash
set -euo pipefail
SRC_ZONE="iot-zone-id"
DST_ZONE="internal-zone-id"
# 1. Snapshot current order
BEFORE=$(unifly firewall policies reorder \
--source-zone "$SRC_ZONE" --dest-zone "$DST_ZONE" --get -o json)
echo "Current order:"
echo "$BEFORE" | jq .
# 2. Compute new order (move the last policy to the top)
NEW_ORDER=$(echo "$BEFORE" | jq -r '[.[-1]] + .[:-1] | join(",")')
# 3. Apply the new order
unifly firewall policies reorder \
--source-zone "$SRC_ZONE" --dest-zone "$DST_ZONE" \
--set "$NEW_ORDER"
echo "Reordered."DNS Ad-Blocking via Policies and Traffic Lists
Unique to unifly's coverage: use a traffic matching list of bad domains and a DNS policy to redirect them. Most competing tools cannot touch the modern Policy Table.
# 1. Create a traffic list of domains (or use IPs/ports)
unifly traffic-lists create \
--name "AdDomains" \
--list-type ipv4 \
--values "0.0.0.0,127.0.0.1"
# 2. Create DNS records that sinkhole each bad domain
# (driven from a curated blocklist file)
while read -r domain; do
[ -z "$domain" ] || [[ "$domain" =~ ^# ]] && continue
unifly dns create \
--domain "$domain" \
--record-type A \
--value "0.0.0.0" \
--ttl 300
done < blocklist.txtCafe / Airbnb Voucher Flow
Generate vouchers, export as a printable list with QR codes.
#!/usr/bin/env bash
set -euo pipefail
VOUCHERS=$(unifly hotspot create \
--name "Cafe-$(date +%Y-%m-%d)" \
--count 20 \
--minutes 1440 \
--up-rate 5000 --down-rate 20000 \
-o json)
# Extract codes
echo "$VOUCHERS" | jq -r '.[].code' > vouchers.txt
# Build a printable markdown table
{
echo "| Code | Duration | Bandwidth |"
echo "| ---- | -------- | --------- |"
echo "$VOUCHERS" | jq -r '.[] | "| \(.code) | 24h | 20 Mbps |"'
} > vouchers.md
# Clean up unused vouchers after 7 days
unifly hotspot purge --filter "status.eq('UNUSED') && created_at.lt('$(date -d '7 days ago' -Iseconds)')"Device Fleet Operations
Fleet Firmware Upgrade (Staggered)
# Upgrade all online switches with 30s between operations
unifly devices list --all -o json | \
jq -r '.[] | select(.type == "USW" and .state == "ONLINE") | .mac' | \
while read -r mac; do
echo "Upgrading $mac..."
unifly devices upgrade "$mac" --yes
sleep 30
doneAdopt All Pending Devices
unifly devices pending -o json | jq -r '.[].mac' | \
xargs -n1 unifly devices adoptPoE Port Reset for a Stuck Device
# Power-cycle port 5 on a specific switch
unifly devices port-cycle "switch-mac" 5Locate a Device in a Rack
# Blink the LED
unifly devices locate "aa:bb:cc:dd:ee:ff" --on true
# Stop blinking
unifly devices locate "aa:bb:cc:dd:ee:ff" --on falseClient Management
Find Clients Quickly
# Substring match against IP, name, hostname, MAC
unifly clients find "macbook"
unifly clients find "10.4.22"
unifly clients find "dc:a6:32" # vendor MAC prefix (Raspberry Pi)Block Unknown Clients by MAC Prefix Allowlist
ALLOWED_PREFIX="aa:bb:cc"
unifly clients list --all -o json | \
jq -r --arg prefix "$ALLOWED_PREFIX" \
'.[] | select(.mac | startswith($prefix) | not) | .mac' | \
xargs -n1 unifly clients blockIsolate a Compromised Client
MAC="aa:bb:cc:dd:ee:ff"
# 1. Block the client immediately
unifly clients block "$MAC"
# 2. Kick from WiFi (if wireless)
unifly clients kick "$MAC"
# 3. Gather forensic context from recent events
unifly events list --within 1 -o json | \
jq --arg mac "$MAC" '.[] | select(.client // "" | contains($mac))'Monitoring and Alerting
Health Check Script
#!/usr/bin/env bash
set -euo pipefail
HEALTH=$(unifly system health -o json)
STATUS=$(echo "$HEALTH" | jq -r '.status // "unknown"')
if [ "$STATUS" != "ok" ]; then
echo "ALERT: Site health is $STATUS"
# Hand off to alerting (PagerDuty, Slack, etc.)
fi
# Check for offline devices
OFFLINE=$(unifly devices list --all -o json | \
jq '[.[] | select(.state == "OFFLINE")] | length')
if [ "$OFFLINE" -gt 0 ]; then
echo "ALERT: $OFFLINE devices offline"
unifly devices list --all -o json | \
jq '.[] | select(.state == "OFFLINE") | {name, mac, last_seen}'
fiTop Bandwidth Consumers
# Top 10 clients by total traffic (last 24h)
unifly stats client --interval daily -o json | \
jq 'sort_by(-(.rx_bytes + .tx_bytes)) | .[0:10] | .[] | {mac, rx_bytes, tx_bytes}'DPI Traffic Breakdown
# Top 10 applications by traffic
unifly stats dpi --group-by by-app -o json | \
jq 'sort_by(-.bytes) | .[0:10]'
# Enable DPI first if needed
unifly dpi status
# If disabled: unifly dpi enableBackup and Recovery
#!/usr/bin/env bash
set -euo pipefail
# Create a backup
unifly system backup create --yes
# Wait and download the latest
sleep 30
LATEST=$(unifly system backup list -o json | jq -r '.[0].filename')
unifly system backup download "$LATEST" --path ./backups/
echo "Backup saved: ./backups/$LATEST"
# Rotate: keep the 5 most recent
unifly system backup list -o json | \
jq -r '.[5:] | .[].filename' | \
xargs -n1 -I{} unifly system backup delete "{}" --yesSecurity Audit
Firewall Policy Audit
# All allow policies (potential exposure)
unifly firewall policies list -o json | \
jq '.[] | select(.action == "ALLOW") | {description, source_zone, dest_zone}'
# Policies without logging (blind spots)
unifly firewall policies list -o json | \
jq '.[] | select(.logging == false) | {id, description}'
# Detect open WiFi SSIDs
unifly wifi list -o json | \
jq '.[] | select(.security == "open") | {name, id}'Unused Network Detection
unifly networks list --all -o json | jq -c '.[]' | while read -r net; do
NET_ID=$(echo "$net" | jq -r '.id')
NAME=$(echo "$net" | jq -r '.name')
REFS=$(unifly networks refs "$NET_ID" -o json 2>/dev/null)
if [ "$(echo "$REFS" | jq 'length')" -eq 0 ]; then
echo "Unused network: $NAME ($NET_ID)"
fi
doneMulti-Controller Operations
Manage multiple controllers via named profiles. Credentials live in the OS keyring per profile.
# Cross-controller health snapshot
for profile in home office warehouse; do
echo "=== $profile ==="
unifly -p "$profile" system health -o json | jq '{status, cpu, mem}'
unifly -p "$profile" devices list --all -o json | jq '[.[] | {name, state}]'
done
# Or via env var
for profile in home office warehouse; do
UNIFI_PROFILE="$profile" unifly system info
doneTUI Handoff Pattern
Unique to unifly: propose a change, let a human visually confirm in the TUI before committing.
#!/usr/bin/env bash
# Agent proposes a firewall rule change
echo "Proposed change:"
cat << 'EOF'
- Policy: Block IoT to Management
- Source zone: IoT
- Dest zone: Internal
- Action: block
EOF
echo "Open the TUI and review screen 5 (Firewall)."
echo "Press Enter when ready to apply, Ctrl-C to abort."
read -r
unifly firewall policies create \
--name "Block IoT to Management" \
--action block \
--source-zone "$IOT_ZONE" \
--dest-zone "$INTERNAL_ZONE" \
--loggingRaw API Escape Hatch
When unifly does not wrap an endpoint yet, fall back to unifly api. It routes through the Session client with CSRF handling.
# Query raw traffic flow statistics (Session v2 endpoint)
unifly api "v2/api/site/default/traffic-flow-latest-statistics" -o json | \
jq '.[] | {mac, rx, tx}'
# Force-reconnect a client via Session command endpoint
unifly api "cmd/stamgr" -m post -d '{"cmd":"kick-sta","mac":"aa:bb:cc:dd:ee:ff"}'
# Raw Integration API call
unifly api "integration/v1/sites/default/clients" -o jsonError Handling Patterns
Retry with Exponential Backoff
retry_unifly() {
local max_attempts=3
local delay=5
local attempt=1
while [ $attempt -le $max_attempts ]; do
if output=$("$@" 2>&1); then
echo "$output"
return 0
fi
echo "Attempt $attempt failed, retrying in ${delay}s..." >&2
sleep $delay
delay=$((delay * 2))
attempt=$((attempt + 1))
done
echo "Failed after $max_attempts attempts" >&2
return 1
}
retry_unifly unifly devices list -o jsonRead Before Write
# Confirm entity exists before mutating
NETWORK=$(unifly networks get "$NETWORK_ID" -o json 2>/dev/null) || {
echo "Network $NETWORK_ID not found" >&2
exit 1
}
# Safe to proceed
unifly networks update "$NETWORK_ID" --name "Updated Name"Fresh Login After Password Rotation
# Force a fresh Session login, bypassing the session cache
unifly --no-cache devices listAgent Best Practices
1. Inspect before mutating. Always get or list an entity before create, update, or delete. 2. Capture IDs from create operations. unifly ... create -o json | jq -r '.id'. 3. Verify after changes. Re-fetch to confirm state. 4. Stagger bulk device operations. Add sleep between restarts, upgrades, and port cycles to avoid overwhelming the controller. 5. Always pass `-o json` in automation. Never parse tables. 6. Always pass `--all` on list commands for enumeration to defeat the 25-row default limit. 7. Use profiles (-p home) to target different controllers without reconfiguring. 8. Check `unifly config show` to confirm the resolved auth mode before running commands that depend on Session or Integration specifically. 9. For destructive operations (delete, reboot, poweroff, purge), summarize the planned change to the user before running even with --yes. 10. Use `--from-file` for complex creates. Payload files are easier to review, version, and replay than long flag lists.