Unofficial UniFi Protect Python API and CLI
Documentation: https://uiprotect.readthedocs.io
Source Code: https://github.com/uilibs/uiprotect
About
Unofficial Python API and CLI for UniFi Protect, for any UniFi OS console that runs Protect. It is the library behind the Home Assistant UniFi Protect integration and can be used on its own. uiprotect is not affiliated with or endorsed by Ubiquiti.
The library is moving to Ubiquiti's documented Public Integration API, which authenticates with an API key. Features the public API does not offer use the undocumented private API, which can change between Protect releases.
Reporting issues and requesting features
Check the public API first
The Public Integration API documentation lists every device field and control the library can support. If a field or control is not there, uiprotect cannot add it as a new feature.
A field is not enough when only some models have the feature: there also has to be a feature flag (under featureFlags in the output) or another field that tells which devices support it. Otherwise Home Assistant would offer controls that do nothing on the other models.
To see what your console returns, query it with curl and an API key. Replace HOST with the IP address or hostname of your console and API_KEY with your key. The command saves the answer to cameras.json in the current folder.
Linux and macOS:
curl -k -H "X-API-KEY: API_KEY" -o cameras.json https://HOST/proxy/protect/integration/v1/cameras
Windows:
curl.exe -k -H "X-API-KEY: API_KEY" -o cameras.json https://HOST/proxy/protect/integration/v1/cameras
For other devices, replace cameras at the end of the URL with another endpoint from the documentation, such as lights, sensors or cameras/CAMERA_ID for a single camera, using its id from cameras.json. Doorbells are cameras. -k skips certificate verification. It is needed while the console uses its default self-signed certificate.
If the file only contains an error, check the key and HOST. Otherwise open it in a text editor and remove MAC addresses, IP addresses and serial numbers before you post it. Never post the command itself, it contains your API key.
Home Assistant diagnostics are no substitute. Their public_bootstrap section only contains fields uiprotect already reads, so a field the library does not know yet is missing there. The bootstrap section is the private API.
Requirements
- UniFi Protect 7.2+, tested against the latest stable release
- Python 3.11+
- PyAV, installed as a dependency, for talkback audio
Windows is not actively supported and not tested in CI, although it should work. Issues about Windows are welcome as a hint, but there is no promise of a fix. The Docker container and WSL are the supported options there.
Installation
From PyPI
pip install uiprotect
To use the command-line interface, install the cli extra:
pip install "uiprotect[cli]"
From GitHub
pip install "uiprotect[cli] @ git+https://github.com/uilibs/uiprotect.git"
Using Docker Container
The Docker container needs no local Python. Add this to your .bashrc or similar. It passes the environment variables from your shell into the container:
function uiprotect() {
docker run --rm -it \
-e UFP_API_KEY \
-e UFP_USERNAME \
-e UFP_PASSWORD \
-e UFP_ADDRESS \
-e UFP_PORT \
-e UFP_SSL_VERIFY \
-e TZ \
-v $PWD:/data ghcr.io/uilibs/uiprotect:latest "$@"
}
- Update with
docker pull ghcr.io/uilibs/uiprotect:latest. $PWDis mounted to/data. Files written anywhere else are lost when the container exits.- Images exist for
linux/amd64andlinux/arm64, so Docker Desktop on macOS and Windows works. TZsets the time zone the CLI uses for local times. Use the time zone name your Protect console uses. The command line docs list the other environment variables.
Authentication
API key
Create an API key on the console under Settings > Control Plane > Integrations with an administrator account. Ubiquiti's Getting Started guide describes it under "Obtaining an API Key". There is no read-only key, so treat it like a password.
Local user
Features the public API does not offer also need a local user with access to Protect.
Quickstart
CLI
export UFP_ADDRESS=YOUR_CONSOLE_IP
export UFP_API_KEY=YOUR_API_KEY
# set to true if the console has a certificate your machine trusts
export UFP_SSL_VERIFY=false
uiprotect cameras list-ids
In Windows PowerShell, set the variables with $env:UFP_API_KEY = "YOUR_API_KEY" and so on.
With only an API key, the CLI runs in public-only mode, and commands without a public equivalent, such as uiprotect nvr, stop with an error. With a local user set as well, the CLI logs in and the device commands read the private API instead:
export UFP_USERNAME=YOUR_USERNAME
export UFP_PASSWORD=YOUR_PASSWORD
uiprotect --help lists all commands and uiprotect <command> --help their options. The command line docs cover them in detail.
Python
The library is async. Its entry point is uiprotect.ProtectApiClient. A client with only an API key uses the public API:
import asyncio
from uiprotect import ProtectApiClient, ProtectDeviceChange
def on_device(change: ProtectDeviceChange) -> None:
print(change.change, change.device_id, sorted(change.changed_fields))
async def main() -> None:
protect = ProtectApiClient.public_only(
"YOUR_CONSOLE_IP", 443, "YOUR_API_KEY", verify_ssl=False
)
try:
await protect.subscribe_devices_and_prime(on_device)
for camera in protect.public_bootstrap.cameras.values():
print(camera.id, camera.name)
await asyncio.sleep(60)
finally:
await protect.async_disconnect_ws()
await protect.close_session()
asyncio.run(main())
In this mode the private entry points (update(), authenticate(), get_bootstrap()) raise PublicOnlyModeError. A rejected key raises NotAuthorized on HTTP calls and turns the websockets to WebsocketState.AUTH_FAILED.
For features that still need the private API, pass the local user as well and call both update() and update_public():
protect = ProtectApiClient(
"YOUR_CONSOLE_IP",
443,
"YOUR_USERNAME",
"YOUR_PASSWORD",
api_key="YOUR_API_KEY",
verify_ssl=False,
)
Subscriptions
subscribe_events delivers typed (ProtectEvent, EventChange) pairs for detection, sensor, alarm hub and access events. subscribe_devices delivers a ProtectDeviceChange for each added, updated or removed device. Devices that already exist are not announced, read them from public_bootstrap after priming. Both need a prior update_public(). subscribe_events_and_prime() and subscribe_devices_and_prime() do both in the right order.
Callbacks are plain functions called on the event loop. They must not block and must not be async def. Exceptions in a callback are logged and swallowed.
The usage docs describe events, device changes, reconnects and the other public API features in detail.
Contributing
Please open an issue and agree on the approach before implementing anything.
Using AI is fine, but you have to drive it. We use AI tools ourselves, so an
unreviewed AI-generated PR or issue only moves the review work to us. AI-assisted
contributions are welcome when you understand the architecture
(AGENTS.md) and the
approach was agreed in an issue first. Anything else will be closed.
What helps most is what AI cannot supply: tests on devices we do not own, sanitized payloads from them, and first-hand knowledge of how they behave.
Developer Setup
The recommended way to develop is using the provided devcontainer with VS Code:
- Install VS Code and the Dev Containers extension
- Open the project in VS Code
- When prompted, click "Reopen in Container" (or use Command Palette: "Dev Containers: Reopen in Container")
- The devcontainer will automatically set up Python, Poetry, pre-commit hooks, and all dependencies
Alternatively, if you want to develop natively without devcontainer:
# Install dependencies (--all-extras installs the cli extra for CLI tests)
poetry install --with dev --all-extras
# Install pre-commit hooks
poetry run pre-commit install --install-hooks
# Run tests
poetry run pytest
# Run pre-commit checks manually
poetry run pre-commit run --all-files
History
This project was split off from pyunifiprotect because that project changed its license to one that would not be accepted in Home Assistant. This project is committed to keeping the MIT license.
Credits
- Bjarne Riis (@briis) for the original pyunifiprotect package
- Christopher Bailey (@AngellusMortis) for maintaining the pyunifiprotect package
Metadata
Release files for uiprotect 23.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| uiprotect-23.2.0.tar.gz | 196.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| uiprotect-23.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 415.5 kB
Release files / uiprotect-23.2.0.tar.gz
| Download URL | uiprotect-23.2.0.tar.gz |
|---|---|
| Size | 196.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6389028ec24807667a1dfab730d5defb3f5a20401dbc9080150a9274e8121b0
|
|
BLAKE2b-256 checksum How to use checksums |
45d68b57cddde7533605f423382cb50af83dedbd9ab3420ac37a7991268e572b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / uiprotect-23.2.0-py3-none-any.whl
| Download URL | uiprotect-23.2.0-py3-none-any.whl |
|---|---|
| Size | 218.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
857de4c1bc20a2bbe343aad03711552adf2f53b9c32f9435b1d918960de96311
|
|
BLAKE2b-256 checksum How to use checksums |
55c941fedfdb3d2e61e06a0cb579abfab496dad8e21098e4adb3534e1ebb2996
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log