# UniFi Site Manager cloud API + connector (remote, outside-UOS access)

Empirically mapped 2026-06-17 against ACG's live account key (vault
`services/unifi-site-manager`), corroborated by Grok + Gemini web search.
This is the "access UniFi outside the UOS environment" path, and it reaches
**UOS-parity depth** for the whole fleet remotely.

Backend script: `scripts/gw-sitemanager.sh`. Base host: `https://api.ui.com`.
Auth: header `X-API-KEY: <key>` (account-level Site Manager key, owner mike@). Read-heavy;
some POST actions exist (device restart, client block) - not wired in yet.

## Three tiers

### Tier 1 - Site Manager API (`/v1/...`, cloud, fleet overview)
| Endpoint | Returns |
|---|---|
| `GET /v1/hosts` | All consoles (id, hardwareId, **ipAddress** WAN, type, owner, reportedState incl. `controllers[]` with each app's `integrationApis` location). 36 for ACG. |
| `GET /v1/hosts/{id}` | One console detail. |
| `GET /v1/sites` | Sites across hosts (82): `meta`, `statistics.counts` (device/client counts), `statistics.gateway` (IPS mode/signature), `ispInfo` (ASN/provider), `percentages.txRetry`. |
| `GET /v1/devices` | Per-host device inventory: name, model, **ip**, status, firmware, uptime, adoption. No RF. |
| `GET /v1/isp-metrics/{5m\|1h}` | Per host/site WAN time-series `periods[]` (~264): `metricTime` + `data.wan` {avgLatency, maxLatency, download_kbps, upload_kbps, packetLoss, uptime, downtime}. Newer gateways (UCG/UDM-SE) populate it; older UDM Pro may report zeros. |
| `GET /v1/sd-wan-configs` | SD-WAN configs (0 for ACG). |

`/ea/*` mirrors most of these. Response wrapper: `{data, httpStatusCode, traceId}` (Site
Manager) ; sub-resource paths like `/v1/hosts/{id}/devices` 404 - use the flat collections
and filter by `hostId`.

### Tier 2 - Cloud CONNECTOR proxy -> console LOCAL Network API (UOS PARITY)
The key unlock. The account key proxies into each console's local UniFi Network API
**remotely**, no LAN/VPN:

```
https://api.ui.com/v1/connector/consoles/{consoleId}/proxy/network/<local-path>
```
`{consoleId}` = the host id from `/v1/hosts`. Two sub-surfaces both work:

- **Official Integration API** - `/proxy/network/integration/v1/...`
  - `/info`, `/sites` (returns site UUIDs), `/sites/{uuid}/devices`, `/sites/{uuid}/devices/{id}`,
    `/sites/{uuid}/devices/{id}/statistics/latest`, `/sites/{uuid}/clients`, `/clients/{mac}`.
  - Device `interfaces.radios[]`: frequencyGHz, channel, channelWidthMHz, wlanStandard; live stats add txRetriesPct.
  - Clean/supported but **shallower** than the internal API (client list lacks RSSI in v1 on Net 10.4).
  - Has POST actions: device `/restart`, client `/block` `/unblock`.

- **Internal stat API** - `/proxy/network/api/s/{site}/stat/...` (site = short name, usually `default`)
  - `GET .../stat/device` -> per-device `radio_table_stats[]`: **cu_total** (channel utilization =
    airtime), cu_self_rx, cu_self_tx, channel, last_channel, bw, **tx_power**, num_sta, satisfaction,
    tx_retries, tx_retries_pct, ast_txto/ast_cst/ast_be_xmit. == the UOS Mongo `ace_stat` depth.
  - `GET .../stat/sta` -> per-client: **rssi, signal, noise**, satisfaction, channel, radio, essid,
    tx_rate, rx_rate, tx_retries, anomalies.
  - `GET .../v2/api/site/{site}/...` also reachable.
  - This is what `gw-sitemanager.sh net <console> radios|clients` uses.

### Tier 3 - the OLD path (UOS Mongo `ace`/`ace_stat`)
Still used by the rest of the unifi-wifi skill (`audit-site.sh`, `model-rank.sh`, etc.) for
UOS-ADOPTED sites. Tier 2 now gives the same data for NON-UOS consoles (and the whole fleet).

## Parity verdict
- Tier 1 alone: inventory + health + WAN/ISP telemetry. NOT RF/per-client.
- **Tier 2 (connector -> internal stat API): full UOS parity** - per-radio airtime/channel/
  tx-power and per-client RSSI/satisfaction - for ANY of the 36 consoles, remotely, with the
  one account key. Broader coverage than UOS (UOS only sees UOS-adopted sites).

## Examples
```bash
S=.claude/skills/unifi-wifi/scripts/gw-sitemanager.sh
bash $S fleet                       # all 36 consoles: WAN IP, model, #devices, online
bash $S devices "Brooklyn/Skybar"   # device inventory for a console
bash $S sites brooklyn              # site health (counts, IPS, ISP)
bash $S isp "CGU-Curves" 5m 12      # WAN latency/throughput history
bash $S net brooklyn radios         # DEEP per-AP airtime/channel/txpower (parity)
bash $S net brooklyn clients        # DEEP per-client RSSI/signal/satisfaction
bash $S net brooklyn raw /integration/v1/sites   # proxy any /proxy/network/... path
```

## Analysis on non-UOS consoles (model-rank / optimize-radios)
The existing airtime-reduction analyses run against cloud-connector data via a `--console` flag
(UOS-Mongo path unchanged). They pull `/stat/report/hourly.ap` (same schema as `stat_hourly`)
through the connector and run the SAME logic via `scripts/rf-analyze.py`:
```bash
bash .claude/skills/unifi-wifi/scripts/model-rank.sh    --console "Brooklyn/Skybar" 7  ng
bash .claude/skills/unifi-wifi/scripts/optimize-radios.sh --console "Brooklyn/Skybar" 14 ng
```
Roam graph is usually EMPTY on small/stationary sites (no `/stat/event` roam log) -> model-rank
ranks by airtime pressure only; optimize-radios returns power-down candidates and 0 disables
(coverage-safe). For disables, supply `NEIGHBOR_JSON` (AP-to-AP SNR from neighbor-collect) as on UOS.

**Multi-site controllers** (e.g. the self-hosted "UOS Server" host runs 47 sites): pass `--site <name>`
(the internal short name from `/api/self/sites`, e.g. Cascades = `va6iba3v`). Default is `default`.
```bash
bash .../model-rank.sh    --console "UOS Server" --site va6iba3v 7 ng    # Cascades 2.4GHz
```
**Gotcha (handled in rf-analyze.py):** the internal `/stat/report/*.ap` endpoint returns only a
small DEFAULT subset of APs unless you POST `macs:[<all uap macs>]` (Cascades returned 10 of 77
until the MAC list was supplied). The analyzer fetches the uap macs from `/stat/device` and passes them.

**Validation (2026-06-17):** Cascades via the connector matched the UOS-Mongo path - 75 APs profiled,
2.4GHz util 65-90% / interf 53-78% / ~1 client each, all power-down, 0 disables. Apples-to-apples (same
self-hosted controller + site). Connector lacks the roam graph, so disables need `NEIGHBOR_JSON`.

### Disables on a remote/non-UOS site (neighbor-collect --console)
`neighbor-collect.sh` builds the AP-to-AP SNR matrix (the only source of managed-AP redundancy; it's
NOT in any API - SSH-to-`/proc/ui_neighbor` only). It now gets its name/BSSID/IP map from the connector:
```bash
bash .../neighbor-collect.sh --console "<name>" [--site <short>] clients/<slug>/unifi-ap-ssh [snr_min]
NBR_JSON=adj.json bash .../neighbor-collect.sh --console "UOS Server" --site va6iba3v clients/cascades-tucson/unifi-ap-ssh
NEIGHBOR_JSON=adj.json bash .../optimize-radios.sh --console "UOS Server" --site va6iba3v 14 ng   # now gets disables
```
The name map comes from the cloud (no UOS login); the AP SSH harvest still needs L3 reach to the AP mgmt
VLAN (the connector proxies the controller, not SSH to APs). So: connector for airtime everywhere +
neighbor-collect for disables wherever you have AP-VLAN reach. Nothing is lost vs the UOS path.

## Gateway VPN / Teleport (via the connector)
The connector reaches the gateway config surface - `GET/PUT /rest/networkconf`, `/rest/setting`,
`/rest/wlanconf`, `/stat/sysinfo` (all 200). VPN servers/tunnels live in `/rest/networkconf`
(`purpose=remote-user-vpn`, `vpn_type=wireguard-server|openvpn-server`; site-to-site as `*-vpn`).
So VPN configs are READABLE now and WRITABLE in principle (PUT/POST) - but writes are high-stakes
(lockout risk) and must be DRY-RUN + confirm gated like gw-control.sh; not wired in yet.
**Teleport: config IS reachable** (corrected 2026-06-17 - the first probes used the wrong paths
`/rest/teleport`/`/stat/teleport`/`/v1/teleport`). The real surface:
- `GET/PUT /api/s/<site>/rest/setting/teleport` (also under `/get/setting`, key `teleport`) -> reads/writes
  `{enabled, subnet_cidr}`. Confirmed via connector: Brooklyn Teleport `{enabled:true, subnet_cidr:'192.168.1.1/24'}`.
- Invite generate/revoke is reportedly `POST /api/s/<site>/cmd/teleport {"cmd":"generate-invite"|"revoke-invite"}`
  (Gemini + community + pulumiverse-unifi `setting.Teleport`) - NOT tested here; it creates a live VPN access
  link, so gate it as a write. Caveat: Teleport invites are consumable only by the WiFiman app (no native
  WireGuard `.conf` export).
- Reach it via the connector `/v1/connector/consoles/{id}/proxy/network/api/s/<site>/...` (Gemini's
  `/v1/hosts/{id}/proxy` form 404s; grok xsearch returned empty twice on this query - gemini + live probe settled it).

## Gotchas
- Direct WAN SSH/HTTPS to a standalone UDM is usually firewalled (Brooklyn 67.1.139.219:
  22/443/8443 filtered) - the connector is the remote path, not direct.
- Internal stat API uses the site SHORT name (`default`), not the integration UUID. Override
  with `net <c> radios --site <name>` for multi-site consoles.
- Older gateways report empty ISP metrics; radio/client data still works via the connector.
- Key is account-level (owner). Non-owner/org keys may 403 on some endpoints.
- Sources: developer.ui.com (Site Manager + Network), help.ui.com "Getting Started with the
  Official UniFi API", Art-of-WiFi UniFi-API-client (`connect_via_site_manager`).
