Choosing a surface
Internal vs Official — what each API surface is, how to reach it, the capability gate, and which to pick.
A single unifi.Client exposes two of the controller's APIs. They cover overlapping resources with
different shapes, identifiers and requirements. This guide explains both and helps you pick — or
combine them, since one client can drive both surfaces at the same time.
The two surfaces
- Internal — the legacy UniFi Network API this library has always wrapped. Every resource method
(
ListNetwork,CreateUser, …) lives here, reachable directly on the client or viac.Internal(). It is the default: calling a resource method on the client is identical to calling it onc.Internal(). - Official — the official UniFi OpenAPI (
integration/v1), reached viac.Official(). A fluent, per-resource-group surface generated from the controller's own OpenAPI snapshot. It needs a newer controller.
// These two calls are identical — Internal is the default surface.
a, err := c.ListNetwork(ctx, "default")
b, err := c.Internal().ListNetwork(ctx, "default")
_, _ = a, b
// The Official surface is explicit and fluent.
info, err := c.Official().Info().Get(ctx)
if err != nil {
log.Fatal(err)
}
log.Println("controller version:", info.ApplicationVersion)Writing c.Internal() is never required in 2.0.0 — it only documents intent. It pays off ahead of the
3.0.0 default flip: code that already says c.Internal() keeps working unchanged.
The Official capability gate
The Official surface only works on a capable controller, so every Official operation runs a capability
gate first and returns a sentinel error when it can't proceed. Match either with errors.Is:
unifi.ErrOfficialAPIUnavailable— the controller can't serve it: a classic (old-style) controller, a failedGET /v1/infoprobe (a rejected API key surfaces here), or a controller version below 10.1.78.unifi.ErrOfficialAPIDisabled— you opted out viaClientConfig.DisableOfficialAPI.
info, err := c.Official().Info().Get(ctx)
switch {
case errors.Is(err, unifi.ErrOfficialAPIDisabled):
log.Println("Official API turned off via DisableOfficialAPI")
return
case errors.Is(err, unifi.ErrOfficialAPIUnavailable):
log.Println("controller too old, classic, or info probe failed — Official API unavailable")
return
case err != nil:
log.Fatal(err)
}
log.Println("controller version:", info.ApplicationVersion)The first successful gate check probes GET /v1/info once and caches the result, so later calls skip it.
Opting out
Set DisableOfficialAPI to skip the Official surface entirely — the capability probe never runs and every
c.Official() operation fails fast with ErrOfficialAPIDisabled:
c, err := unifi.NewClient(&unifi.ClientConfig{
URL: "https://unifi.example.com",
APIKey: "your-api-key",
DisableOfficialAPI: true,
})Site identifiers differ
The biggest day-to-day difference is how each surface names a site:
| Surface | Site identifier | Example |
|---|---|---|
| Internal | site name string | "default" |
| Official | a uuid.UUID | c.Official().Sites().ResolveID(ctx, "default") |
Official().Sites().ResolveID(ctx, name) maps the familiar name to its UUID (and caches it). If you already
hold a UUID string, parse it with uuid.Parse from github.com/google/uuid. See the
Sites guide for the full bridge.
The 2.0 → 3.0 default flip
In 2.0.0 the Internal surface is the canonical default, so existing code is untouched. 3.0.0 is
expected to flip the default to the Official client. If you want to stay on the legacy API across that
change, call resource methods through c.Internal() now — it is a no-op today and future-proof later.
Which should I use?
This is not a mutually exclusive, either/or choice. Both surfaces live on the same client and can be used side by side — Internal for one resource, Official for another — in the same program. There is no mode to toggle and nothing to re-initialise; just call whichever surface you want:
// One client, two surfaces, used side by side.
// Internal stays keyed by the site name:
nets, err := c.ListNetwork(ctx, "default")
if err != nil {
log.Fatal(err)
}
// Official is keyed by a uuid.UUID, resolved from that same name:
siteID, err := c.Official().Sites().ResolveID(ctx, "default")
if err != nil {
log.Fatal(err)
}
info, err := c.Official().Info().Get(ctx)
if err != nil {
log.Fatal(err)
}
log.Printf("%d networks; official site %s on controller %s", len(nets), siteID, info.ApplicationVersion)Because mixing is supported, decide deliberately which surface owns which resource rather than reaching for whichever is closest to hand. Consistency is what keeps a codebase legible:
- Prefer Official for resources it covers — it is the direction Ubiquiti supports going forward, with stable typed models and explicit pagination. Requires controller 10.1.78+.
- Use Internal for everything the Official surface doesn't cover yet, or when you target controllers between 9.0.114 and 10.1.78. It has the broadest resource coverage.
- Keep each resource on one surface wherever you can. The two expose different models and identifiers
for the same thing — site name vs
uuid.UUID, differing field shapes — so reading on one surface and writing on the other invites drift and surprising mismatches.
The two surfaces are independent views, not a shared store: the same object fetched via Official and via Internal comes back as different Go types with their own fields and IDs. Don't assume a value from one will round-trip through the other — translate explicitly (for example, resolve a site name to its UUID) on the rare occasions you genuinely need to cross over.
The UniFi API reference and the Official reference document the Official surface endpoint-by-endpoint; the Resources catalogue lists what the Internal surface exposes.