Async Python client for the UniFi Access API
Project description
aiounifiaccess
Async Python client for the UniFi Access API.
- Full REST API coverage — all endpoints from API v4.0.10
- Real-time events — WebSocket listener with auto-reconnect and typed event models
- Webhook receiver — built-in HTTP server for webhook events with signature verification
- Idempotent webhook registration —
ensure_endpoint()andsetup_webhook()handle subscription management - Pydantic v2 models — fully typed request/response objects
- Async-native — built on aiohttp for composability with other async libraries
Installation
pip install aiounifiaccess
Quick Start
import asyncio
import os
from aiounifiaccess import UniFiAccessClient
async def main():
async with UniFiAccessClient(
host=os.environ["UNIFI_ACCESS_HOST"],
api_token=os.environ["UNIFI_ACCESS_TOKEN"],
) as client:
# List all users
users, pagination = await client.users.list()
for user in users:
print(f"{user.first_name} {user.last_name} ({user.status})")
# Get a specific door
door = await client.doors.get("door-id-here")
print(f"{door.name}: {door.door_lock_relay_status}")
# Remote unlock
await client.doors.unlock("door-id-here", actor_name="API Script")
asyncio.run(main())
Real-Time Events
UniFi Access delivers events through two channels:
| Channel | Delivery | Event types |
|---|---|---|
| WebSocket | Persistent connection | Doorbell rings, remote unlocks, connection status |
| Webhook | Controller POSTs to your endpoint | Door unlocks (NFC/PIN/fingerprint), DPS status, schedules, visitors |
Notably, credential-based door unlock events (access.door.unlock) are webhook-only — they are not delivered over the WebSocket. To receive all event types, you need both channels.
WebSocket Only
If you only need doorbell and remote unlock events:
import asyncio
import os
from aiounifiaccess import UniFiAccessClient, RemoteViewEvent
async def main():
async with UniFiAccessClient(
host=os.environ["UNIFI_ACCESS_HOST"],
api_token=os.environ["UNIFI_ACCESS_TOKEN"],
) as client:
@client.on(RemoteViewEvent)
async def handle_doorbell(event: RemoteViewEvent):
print(f"Doorbell ring at {event.data.door_name}")
await client.listen()
asyncio.run(main())
WebSocket + Webhook (Recommended)
To receive all events including credential-based door unlocks, use setup_webhook(). This registers a webhook subscription on the UniFi controller, starts a local HTTP receiver, and runs both channels concurrently:
import asyncio
import os
from aiounifiaccess import (
UniFiAccessClient,
DoorUnlockEvent,
DoorPositionEvent,
RemoteViewEvent,
)
async def main():
async with UniFiAccessClient(
host=os.environ["UNIFI_ACCESS_HOST"],
api_token=os.environ["UNIFI_ACCESS_TOKEN"],
) as client:
# Register webhook on the controller and start local receiver.
# The controller will POST events to this URL.
await client.setup_webhook("https://myserver:8080/webhook")
@client.on(DoorUnlockEvent)
async def handle_unlock(event: DoorUnlockEvent):
actor = event.data.actor.name
door = event.data.location.name
method = event.data.object.authentication_type
print(f"{actor} unlocked {door} via {method}")
@client.on(DoorPositionEvent)
async def handle_dps(event: DoorPositionEvent):
print(f"{event.data.location.name}: {event.data.object.status}")
@client.on(RemoteViewEvent)
async def handle_doorbell(event: RemoteViewEvent):
print(f"Doorbell ring at {event.data.door_name}")
# Runs both WebSocket and webhook receiver until stopped
await client.listen()
asyncio.run(main())
setup_webhook() is idempotent — if a webhook subscription already exists for the same URL and event set, it reuses it. If the URL matches but the events differ, it updates the existing subscription. By default it subscribes to all known webhook event types.
Subscribing to Specific Events
Use WebhookEventType to subscribe to only the events you need:
from aiounifiaccess import WebhookEventType
await client.setup_webhook(
"https://myserver:8080/webhook",
events=[
WebhookEventType.DOOR_UNLOCK,
WebhookEventType.DEVICE_DPS_STATUS,
],
)
Available event types:
| Enum value | Event string | Description |
|---|---|---|
DOOR_UNLOCK |
access.door.unlock |
All door unlock events (NFC, PIN, fingerprint, remote) |
DEVICE_DPS_STATUS |
access.device.dps_status |
Door position sensor changes |
DOORBELL_INCOMING |
access.doorbell.incoming |
Doorbell ring |
DOORBELL_COMPLETED |
access.doorbell.completed |
Doorbell accepted/declined/cancelled |
DOORBELL_INCOMING_REN |
access.doorbell.incoming.REN |
Request-to-Enter button |
DEVICE_EMERGENCY_STATUS |
access.device.emergency_status |
Emergency mode changes |
UNLOCK_SCHEDULE_ACTIVATE |
access.unlock_schedule.activate |
Unlock schedule activated |
UNLOCK_SCHEDULE_DEACTIVATE |
access.unlock_schedule.deactivate |
Unlock schedule deactivated |
TEMPORARY_UNLOCK_START |
access.temporary_unlock.start |
Temporary unlock started |
TEMPORARY_UNLOCK_END |
access.temporary_unlock.end |
Temporary unlock ended |
VISITOR_STATUS_CHANGED |
access.visitor.status.changed |
Visitor status changed |
Manual Webhook Configuration
If you prefer to manage webhook registration separately (or already have one registered), pass the secret directly:
async with UniFiAccessClient(
host=os.environ["UNIFI_ACCESS_HOST"],
api_token=os.environ["UNIFI_ACCESS_TOKEN"],
webhook_secret="your_webhook_secret",
webhook_port=8080,
) as client:
# ...handlers...
await client.listen()
Or use the webhook manager API directly:
# Idempotent registration (create or reuse)
endpoint = await client.webhooks.ensure_endpoint(
"https://myserver:8080/webhook",
"my-app",
)
print(f"Secret: {endpoint.secret}")
# Or manual CRUD
endpoints = await client.webhooks.list_endpoints()
await client.webhooks.delete_endpoint(endpoint.id)
Standalone Webhook Receiver
The WebhookReceiver can be used independently of the full client:
import asyncio
from aiounifiaccess import WebhookReceiver, DoorUnlockEvent
from aiounifiaccess.events.handler import EventHandler
handler = EventHandler()
@handler.on(DoorUnlockEvent)
async def handle(event: DoorUnlockEvent):
print(f"{event.data.actor.name} unlocked {event.data.location.name}")
async def main():
receiver = WebhookReceiver("your_webhook_secret", port=8080)
await receiver.listen(handler)
asyncio.run(main())
Webhook Signature Verification
For custom webhook handling outside the built-in receiver:
from aiounifiaccess import verify_webhook_signature
is_valid = verify_webhook_signature(
secret="your_webhook_secret",
signature_header=request.headers["Signature"],
body=await request.read(),
)
Debug Logging
To see all raw incoming messages on either channel:
import logging
# Both channels
logging.getLogger("aiounifiaccess.events").setLevel(logging.DEBUG)
# Or individually
logging.getLogger("aiounifiaccess.events.listener").setLevel(logging.DEBUG) # WebSocket
logging.getLogger("aiounifiaccess.events.receiver").setLevel(logging.DEBUG) # Webhook
API Managers
| Manager | Attribute | Endpoints |
|---|---|---|
| Users | client.users |
29 |
| Visitors | client.visitors |
13 |
| Access Policies | client.access_policies |
15 |
| Credentials | client.credentials |
17 |
| Doors | client.doors |
13 |
| Devices | client.devices |
4 |
| System Logs | client.system_logs |
4 |
| Identity | client.identity |
6 |
| Webhooks | client.webhooks |
5 |
| Server | client.server |
2 |
Compatibility
| Library Version | API Reference Version |
|---|---|
| 0.1.x | 4.0.10 |
Requirements
- Python 3.11+
- aiohttp >= 3.9
- pydantic >= 2.0
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiounifiaccess-0.2.0.tar.gz.
File metadata
- Download URL: aiounifiaccess-0.2.0.tar.gz
- Upload date:
- Size: 33.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4805b54cc21a46689bccb27408119e9db34b2726be72fcb2fb2cc632b2f91964
|
|
| MD5 |
8220fddcc689329f2ad2a46a083022f3
|
|
| BLAKE2b-256 |
b4ace8f0be69905ba5db6155c1a0495006f98a771b888bb75e22ad08311e5ea1
|
Provenance
The following attestation bundles were made for aiounifiaccess-0.2.0.tar.gz:
Publisher:
publish.yml on realworldtech/aiounifiaccess
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiounifiaccess-0.2.0.tar.gz -
Subject digest:
4805b54cc21a46689bccb27408119e9db34b2726be72fcb2fb2cc632b2f91964 - Sigstore transparency entry: 1186557809
- Sigstore integration time:
-
Permalink:
realworldtech/aiounifiaccess@2169756b6bbb6f230d815801fab779d9fc1fd8d7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/realworldtech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2169756b6bbb6f230d815801fab779d9fc1fd8d7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file aiounifiaccess-0.2.0-py3-none-any.whl.
File metadata
- Download URL: aiounifiaccess-0.2.0-py3-none-any.whl
- Upload date:
- Size: 40.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6ddfae3eea5f440a4965e1dc192b4c03b2fe3e7068655d3eedd1a9ba3a4049fb
|
|
| MD5 |
da0b89f7d656f9951986be93ef28d1e3
|
|
| BLAKE2b-256 |
749dee0ebae32394111b052d6b30020fcf44a808e5ac0d82db71e4847e45d8c0
|
Provenance
The following attestation bundles were made for aiounifiaccess-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on realworldtech/aiounifiaccess
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiounifiaccess-0.2.0-py3-none-any.whl -
Subject digest:
6ddfae3eea5f440a4965e1dc192b4c03b2fe3e7068655d3eedd1a9ba3a4049fb - Sigstore transparency entry: 1186557811
- Sigstore integration time:
-
Permalink:
realworldtech/aiounifiaccess@2169756b6bbb6f230d815801fab779d9fc1fd8d7 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/realworldtech
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@2169756b6bbb6f230d815801fab779d9fc1fd8d7 -
Trigger Event:
push
-
Statement type: