UniFi has two API key stores, and using the wrong one costs an hour
200 from the cloud API and 401 from every gateway endpoint, with both header styles. That specific combination has exactly one cause.
If you have ever generated a UniFi API key, watched it work perfectly against one host,
and then watched the same key return 401 Unauthorized against every gateway
endpoint — with both X-API-KEY and Bearer — you had the wrong
kind of key.
The two stores
| Key type | Created at | Works against | Fails against |
|---|---|---|---|
| Local (what you want) | the gateway's own UI → Settings → Control Plane → Advanced → API | /proxy/network/... on the gateway |
api.ui.com |
| Cloud (Site Manager) | unifi.ui.com |
api.ui.com |
the gateway |
The tell is precise, and it is worth memorising: 200 from
api.ui.com/v1/hosts but 401 from every gateway endpoint.
When you see that, stop probing endpoints. The key is fine; it is simply the cloud kind, and
no amount of header variation will help.
Keep them in separate variables. If a script reads one variable for gateway work and a
cloud key is sitting in it, you get 401 where you wanted an honest
"no credentials configured", and you will debug the wrong layer.
Try the Integration API first
The modern surface lives under /proxy/network/integration/v1/, uses the same
local key, and is where the configuration endpoints actually are:
GET /integration/v1/info # applicationVersion
GET /integration/v1/sites # site ids
GET /integration/v1/sites/<id>/devices
GET /integration/v1/sites/<id>/clients
GET /integration/v1/sites/<id>/networks
GET /integration/v1/sites/<id>/dns/policies # the DNS record surface
GET /integration/v1/sites/<id>/firewall/policies
Responses are paged: {offset, limit, count, totalCount, data[]}.
The expensive lesson: we spent real time concluding that this firmware
had no DNS-record endpoint at all, based on the classic API. It exists — as
dns/policies on the Integration API. Probe both surfaces before you conclude an
endpoint is absent.
Adding a site-level DNS record
POST /proxy/network/integration/v1/sites/<id>/dns/policies
{
"type": "A_RECORD",
"domain": "wiki.example.lan",
"ipv4Address": "192.168.1.20",
"ttlSeconds": 300,
"enabled": true
}
Three things cost time here:
- All five fields are required, and the API reports them one at a time.
Send
{"type":"A_RECORD"}and it tells you the next missing one. You will make several requests before it accepts. - The field is
ipv4Address, notvalue. An unknown property returnsUnknown request body property, which is a clear error once you know to look for it. - Clear any per-client record for the same name first. The POST fails
with
api.dns.policy.validation.overlap-with-local-dnswhile a client still holds a local DNS record for that hostname.
A site-level record serves every client using the gateway as resolver, including VPN clients. A per-client record serves only that one client — which is usually not what you meant.
You cannot create an API key from the API
Keys are minted and revoked only in an authenticated admin UI session. Every plausible endpoint returns 404 on both surfaces. That is deliberate: a leaked key cannot mint more keys or revoke yours. The first key always comes from a human.
Handing a key to a script without exposing it
Never paste an API key into a chat window, a ticket, or an email. It lands in history, in
logs, and in whatever the recipient's tooling does with conversation text. Write it to a
0600 file via a hidden prompt, then have the script verify it against the
gateway and report only length and pass/fail — so a wrong type of key is caught
immediately without the value being printed anywhere.
UniFi Network Pack — $29
The full API runbook, the VLAN and firewall-zone design with its access matrix, and the dual-WAN reality.