go-unifi

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 via c.Internal(). It is the default: calling a resource method on the client is identical to calling it on c.Internal().
  • Official — the official UniFi OpenAPI (integration/v1), reached via c.Official(). A fluent, per-resource-group surface generated from the controller's own OpenAPI snapshot. It needs a newer controller.
surfaces.go
// 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 failed GET /v1/info probe (a rejected API key surfaces here), or a controller version below 10.1.78.
  • unifi.ErrOfficialAPIDisabled — you opted out via ClientConfig.DisableOfficialAPI.
gate.go
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:

disable.go
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:

SurfaceSite identifierExample
Internalsite name string"default"
Officiala uuid.UUIDc.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:

combined.go
// 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.

Next steps

On this page