Raw HTTP calls
Reach UniFi endpoints that have no generated method using Do/Get/Post/Put/Patch/Delete, with correct path resolution.
Most resources have a typed method (ListNetwork, CreateUser, …). When an endpoint isn't covered — a brand-new
controller feature, a niche setting, or an Official endpoint without a wrapper yet — the client exposes generic
HTTP helpers. They handle URL building, JSON marshaling, authentication, interceptors, error handling, and
response decoding for you, so you only supply a path and your own request/response types.
The helpers
| Method | Does |
|---|---|
Do(ctx, method, path, reqBody, respBody) | The core call: any HTTP method. The rest wrap it. |
Get(ctx, path, reqBody, respBody) | HTTP GET. |
Post(ctx, path, reqBody, respBody) | HTTP POST. |
Put(ctx, path, reqBody, respBody) | HTTP PUT. |
Patch(ctx, path, reqBody, respBody) | HTTP PATCH (partial update). |
Delete(ctx, path, reqBody, respBody) | HTTP DELETE. |
reqBody is marshaled to JSON (pass nil for none); respBody is decoded from the JSON response (pass nil
to ignore it). Both are any, so use anonymous structs or any existing unifi type. See the full signatures on
pkg.go.dev.
Path resolution
The path argument follows one rule (buildRequestURL):
- No leading slash (site-relative): the path is joined under the controller's primary API prefix. On
new-style (UniFi OS) controllers — the only style supported in 2.0.0 — that prefix is
/proxy/network/api. So"s/default/rest/networkconf"becomes/proxy/network/api/s/default/rest/networkconf. - Leading slash or absolute URL: used as-is, bypassing the prefix entirely. Use this only for paths outside the standard API tree.
The classic /api/... form that worked on old-style controllers does not work on new-style controllers —
use the site-relative form instead. Old-style (classic) controllers aren't supported at all in 2.0.0; the client
fails fast on them.
Reading a resource
var resp struct {
Meta unifi.Meta `json:"meta"`
Data []unifi.Network `json:"data"`
}
// No leading slash => joined under /proxy/network/api on a new-style controller.
if err := c.Get(ctx, "s/default/rest/networkconf", nil, &resp); err != nil {
log.Fatalf("raw get: %v", err)
}
for _, n := range resp.Data {
log.Printf("%s", n.Name)
}Legacy Internal endpoints wrap their payload in a { "meta": {...}, "data": [...] } envelope, so decode into a
struct with Meta and Data fields. unifi.Meta
carries the rc/msg status — and the client already turns an rc: "error" envelope into a returned error for
you, so a successful call means Data is populated.
Creating a resource
payload := struct {
Name string `json:"name"`
Purpose string `json:"purpose"`
}{Name: "iot", Purpose: "corporate"}
var resp struct {
Meta unifi.Meta `json:"meta"`
Data []unifi.Network `json:"data"`
}
if err := c.Post(ctx, "s/default/rest/networkconf", payload, &resp); err != nil {
log.Fatalf("raw post: %v", err)
}The v2 API tree
Some newer resources (firewall zones, zone policies, AP groups, …) live under the v2 API tree at
/proxy/network/v2/api. Rather than hard-coding that, use the exported unifi.NewStyleAPI.ApiV2Path constant.
Because the result starts with a slash, it is sent as-is:
var zones []struct {
ID string `json:"_id"`
Name string `json:"name"`
}
// Leading slash => used as-is, bypassing the primary API prefix.
path := unifi.NewStyleAPI.ApiV2Path + "/site/default/firewall/zone"
if err := c.Get(ctx, path, nil, &zones); err != nil {
log.Fatalf("list zones: %v", err)
}v2 responses are typically bare (no meta/data envelope), so decode directly into your target type.
Reaching the Official API by hand
The Official UniFi OpenAPI lives under /proxy/network/integration/v1. Most of it is already wrapped by the
fluent c.Official() surface — prefer that. But you can hit any path directly with
an absolute (leading-slash) path. A few Official endpoints (e.g. firewall policies) accept PATCH for partial
updates; most — including networks — use PUT:
patch := struct {
Name string `json:"name"`
}{Name: "updated-name"}
var resp struct {
ID string `json:"id"`
Name string `json:"name"`
}
// Firewall policies are the one Official resource that accepts PATCH.
path := "/proxy/network/integration/v1/sites/<site-id>/firewall/policies/<policy-id>"
if err := c.Patch(ctx, path, patch, &resp); err != nil {
log.Fatalf("patch: %v", err)
}Patch sends a literal HTTP PATCH. Only a few Official integration/v1 endpoints (e.g. firewall policies)
accept it — most, including Official networks, require a full PUT, as do legacy Internal REST resources (e.g.
networkconf). Use Put for those, sending the complete object.