UniFi Access
Commercial access-control for Ubiquiti's UniFi Access platform — UA-Hub, UA-Ultra, and Access running on UDM-Pro / UNVR consoles via UniFi OS. Uses the local Developer REST API on the controller with a bearer API token. One GEM device row represents the Access controller; each zone represents a door.
Prerequisites
- UniFi Access 2.x or newer with the Developer API enabled. The API is exposed on the controller (Access hub, UA-Ultra, or UniFi OS console).
- API token. Mint one under Settings → Advanced → API Token. The value is shown once — save it before navigating away. This token authorises every request.
- Network reach.
- Standalone Access hub / UA-Ultra: TCP/12445 reachable from GEM to the controller (HTTPS with a self-signed cert).
- Access on UniFi OS (UDM-Pro, UNVR): TCP/443 reachable, API URL prefixed with
/proxy/access/.
- Doors already enrolled. GEM does not do door onboarding — enrol doors via the UniFi Access mobile / web UI first.
Setup
1. Mint the API token
- Open the UniFi Access web UI.
- Go to Settings → Advanced → API Token.
- Click Create API Token. Name it something like
gem-controller. - Copy the token. This is the only time it will be shown.
2. Add the device in GEM
Under Admin → Devices → Add:
| Field | Value |
|---|---|
| Driver | unifi_access |
| Name | e.g. access_ctrl |
Attribute ip | Controller IP or hostname |
Attribute token | Bearer token from step 1 (auto-encrypted) |
If Access is running on UniFi OS (UDM-Pro / UNVR) rather than a standalone Access hub:
| Attribute | Value |
|---|---|
port | 443 |
api_base | /proxy/access/api/v1/developer |
Save. The driver will hit /doors to verify the token, then start the two polling loops.
3. Discover doors
Run the get_doors command from the Command Console. Each returned entry looks like:
{
"id": "2f2f42d0-1234-4a5b-9c8d-abcdef012345",
"name": "Front Lobby",
"floor_id": "…",
"door_lock_relay_status": "lock",
"door_position_status": "closed",
"is_bind_hub": true
}
The id field is what you use as the zone address.
4. Create zones — one per door
Under Admin → Zones → Add, for each door:
| Field | Value |
|---|---|
| Device | The Access device from step 2 |
| Subsystem | access_control (or door — either works with the state map) |
| Address | Door UUID from get_doors |
Save. The next poll cycle (default 60 s) will populate state, door_position, door_name, and online.
Attributes
Required (device)
| Name | Type | Description |
|---|---|---|
ip | string | LAN IP or hostname of the Access controller. |
token | string (secure) | Bearer API token minted under Access → Settings → Advanced → API Token. |
Optional (device)
| Name | Type | Default | Description |
|---|---|---|---|
port | int | 12445 | TCP port. Use 443 for UniFi OS proxied access. |
api_base | string | /api/v1/developer | API path prefix. Use /proxy/access/api/v1/developer for UniFi OS. |
status_interval | int | 60000 | Milliseconds between door-state polls. |
event_poll_interval | int | 30000 | Milliseconds between event-log polls. 0 disables event polling. |
unlock_seconds | int | 5 | Fallback unlock duration when a zone-level override is not set. Bounded 1-60. Note: Access enforces its own hold time regardless — this is a hint that surfaces on the response payload. |
released_state_ms | int | 5000 | How long zone.state stays at released after an unlock before the next poll snaps it back. |
Optional (zone)
| Name | Type | Description |
|---|---|---|
unlock_seconds | int | Per-door override of the device-level default. |
Populated by the driver
The polling loops write these attributes onto each door zone:
| Name | Source | Notes |
|---|---|---|
state | door_lock_relay_status | locked / unlocked / released (transient). |
door_position | door_position_status | open / closed / unknown. Requires a door-position sensor to be wired to the reader / hub. |
door_name | name | The friendly name from Access. |
online | is_bind_hub | true if the door is bound to a reachable Access hub. |
last_event | /system/logs | Most recent event type (badge_read / door_forced / …). |
last_event_at | /system/logs | ISO timestamp of the event. |
last_event_user | /system/logs | Name of the user attached to the event, if any. |
Commands
| Command | Args | Notes |
|---|---|---|
unlock | address, duration (optional) | Buzz-open the door (one-shot). Duration is a hint — Access enforces its own hold time. |
lock / reset_lock_rule | address | Reset the lock rule to the normal schedule. Locks the door if a keep_unlock was active. |
keep_unlock | address | Hold the door unlocked until reset. Use for propping doors open. |
keep_lock | address | Hold the door locked, ignoring valid credentials. Lock-down. |
get_doors | — | Enumerate all doors. Cache is refreshed with the response. |
get_status | address | Read a single door. |
get_events | since (optional epoch ms) | Read the system log. |
get_users | — | List Access users. |
get_visitors | — | List active visitors. |
refresh | — | Force a door status poll now. |
raw_request | path, method, body | Escape hatch — pass-through to any /api/v1/developer/* endpoint. |
Zone address format
Every zone address is a UniFi Access door UUID (36-character, hyphenated). Get them via get_doors.
Known limitations
- Unlock duration is Access-controlled. The Developer API's
unlockendpoint is a one-shot buzz whose hold time is set by the door schedule in Access, not by anything passed on the API call. To hold a door open longer, usekeep_unlockand laterlock(reset). - Identity provisioning is out of scope. Creating users, issuing credentials, and assigning schedules is not exposed. Manage those in the Access web / mobile UI.
- Event log is polled, not streamed. The Developer API exposes a WebSocket for events, but this driver polls
/system/logswith asince_mswatermark instead. Setevent_poll_interval=0to skip event polling if you only care about door state. - UniFi Identity Enterprise / SSO is not consumed. The driver uses the local API token, not the tenant OAuth.
- UDM-Pro deployments put the API behind
/proxy/access/api/v1/developer. Setport=443andapi_base=/proxy/access/api/v1/developer— the reference implementation was authored against the standalone hub URL first.
Troubleshooting
http 401 — check that the API token is valid. The token was revoked, expired, or copied incorrectly. Mint a new one in the Access UI and paste it into the token attribute.
http 404 on connect. The api_base path is wrong. For a standalone Access hub, /api/v1/developer is correct on port 12445. For UniFi OS deployments, use /proxy/access/api/v1/developer on port 443.
Zone state stays unknown after unlock. The door doesn't have a position sensor wired to the hub. door_lock_relay_status will still reflect lock / unlock; only door_position requires the sensor.
Events stop populating. Check the token still has read scope. If you disabled Developer API access on the controller after adding it here, the polling loop logs a 401 warning each cycle. Set event_poll_interval=0 while token access is out.
get_users returns an empty array but the Access UI shows users. Some tenants scope users by site — the Developer API's /users returns the currently-active site by default. Use raw_request with a site-scoped path if you need cross-site enumeration.
Follow-ups
- Consume the Developer API WebSocket (
/subscribe/events) for push-based events instead of polling. - Expose credential issuance / user provisioning as a separate management surface.
- Surface schedule information on zones (currently only
state/door_positionare polled). - Sample-and-cache door photos where cameras are paired to doors.