Brivo
GEM integrates with Brivo (OnAir / Brivo Access) cloud-based access control. Brivo handles physical access decisions in the cloud; GEM observes events and pushes credentials so the two systems stay in sync.
What the driver does
- Receives Brivo events via webhook (preferred) or polls the access-events feed (fallback when GEM is not publicly reachable). Each event is translated into a normalized
pinorrfidevent so GEM Access Control rules fire — typically to run macros for scenes, lights, alarm bypass, etc. - Pushes GEM users + credentials to Brivo through the access device driver contract (
syncUser,disableUser,deleteUser). PINs are written as a property of the Brivo user record (witheffectiveFrom/effectiveTomirroring GEM'svalid_from/valid_untillifecycle); cards are created as separate Brivo credential records and attached. When you disable, expire, or revoke a GEM user, the access control engine setssuspended=trueon Brivo on the next sweep. - Pulses doors on demand via the
unlockcommand — useful for macros that open a Brivo door without a credential event (e.g. "guest arriving" workflows).
Prerequisites
- A Brivo developer-portal application (
client_id+client_secret). - A per-installation Brivo
api-key(issued by Brivo when you provision the integration). - Either:
- A Brivo admin username + password with API access (simplest — uses the OAuth password grant), or
- A refresh_token obtained out-of-band via the OAuth authorization-code flow (recommended for production).
Setup
-
System > Devices: add a device with driver
brivo. Fill in:client_idclient_secret(secure)api_key(secure)- either
username+password(secure) for the password grant, or run theset_refresh_tokencommand after creation webhook_base_url— the public HTTPS URL where Brivo can reach this GEM instance (e.g.https://gem.example.com). Required for webhook delivery; without it the driver falls back to polling every 5 seconds.
-
Add zones under the device, one per Brivo access point you want to control.
zone.address= the numeric Brivoaccess_pointid. Run theget_access_pointscommand to enumerate available doors. -
Register the webhook by running the
register_subscriptioncommand. Brivo will POST events to{webhook_base_url}/driver_webhook/{device_id}/{token}. The token is generated automatically and stored as a secure attribute. TheencryptedSecretWordBrivo returns is also stored for future signature verification. -
Reference the device in Access Control rules. Set
access_device_idto the Brivo device. When Brivo sees a card swipe or PIN entry, the driver emits apin/rfidevent, GEM matches it to a user, and the rule fires its action.
Authentication flow
Brivo uses 3-legged OAuth2 by default. Two practical paths:
Password grant (simplest)
If your Brivo deployment allows the resource-owner password grant, set username + password on the device. The driver exchanges them for an access_token + refresh_token on connect, then keeps the refresh_token rotated forever.
Authorization-code grant (recommended)
For production, do the auth-code dance once and feed the resulting refresh_token to GEM:
- Direct an admin browser to:
https://auth.brivo.com/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={your_redirect}
- After approval Brivo redirects to
{redirect_uri}?code=.... - Run the GEM command
authorize_codewith thecodeand theredirect_uriyou used. The driver exchanges the code for a refresh_token and stores it encrypted on the device.
Alternatively, if you obtain a refresh_token through any other means (e.g. existing tooling), paste it via the set_refresh_token command.
Webhook security
- The webhook URL embeds a random per-device token in its path. Calls without that token are rejected with 401.
- Brivo also returns an
encryptedSecretWordat subscription time (the account secret word AES/CBC/NoPadding-encrypted with the client_id, dashes removed). The driver stores it for future per-call signature verification. - The webhook URL must be HTTPS in production. Brivo will not deliver events to plain HTTP.
Polling fallback
If webhook_base_url is empty (or use_polling=true is set), the driver instead polls GET /v1/api/access-events?recordsFrom=…&pageSize=100 every event_poll_seconds (default 5) and processes new events the same way. This is suitable for LAN-only deployments without a public ingress.
Mapping Brivo events to GEM users
Brivo events carry the structure documented in the Brivo event-subscription schema — securityAction, actor, eventObject, eventData.credentials[], eventData.unknownWeigand, etc. The driver classifies and routes them as follows:
| Brivo event shape | GEM behavior |
|---|---|
actor.objectType === 'USER' and the user was previously synced from GEM | Look up the GEM user via the brivo_user_id_{device_id} attribute; emit pin / rfid with the GEM user's stored credential so the access_control rule fires normally |
actor.objectType === 'USER' but the user was NOT synced from GEM | Logged via the last_event attribute; no rule fires (the user is Brivo-native and not represented in GEM) |
Unknown / denied swipe — no actor, but eventData.credentials[0].reference_id or eventData.unknownWeigand is set | Emit rfid (or pin, if the action text mentions PIN/keypad) with the raw value so the access_control rule logs it as unknown_credential |
securityAction.action matches "request to exit" / "exit request" / "REX" | Emit rex with the access-point id; no user lookup needed |
securityAction.action matches "Administrator …" / "Manual …" (e.g. action 5064 "Administrator Pulse Output") | Logged but no rule fires — the admin already authorised the action |
eventObject.objectType is anything other than ACCESS_POINT | Logged via last_event only — these are panel / schedule / config audit events, not door activity |
Every event also lands on the last_event device attribute as compact JSON (uuid, actionId, action text, exception flag, access_point_id, user_id, site_id, occurred) so Attribute Triggers can react to specific event types directly.
Quirks
- Brivo is the access controller — it grants/denies access independently of GEM. GEM rules fire as observers; their actions usually run macros (lights, scenes, alarm bypass) rather than re-opening the door.
- Refresh tokens are long-lived but can be invalidated at any time by Brivo (e.g. on password change). If polling/webhooks start erroring 401, re-issue a refresh_token via
authorize_codeorset_refresh_token. - The
held_open_threshold_secrule field is currently informational. Forced-door / held-open detection is on the roadmap.
Commands
| Command | Purpose |
|---|---|
unlock / open / pulse | Pulse a door. args.address = Brivo access_point id. |
snapshot | Return a recent video clip URL for the camera linked to the given access point. Used by the access_log capture chain to attach video to badge-in events. With no args.address, falls back to the most recent event's access point (if it fired in the last 5 s). |
get_access_points | List available access points (use to populate zone addresses). |
get_cameras | List all cameras on the Brivo account. |
get_camera | Retrieve a single camera (set args.show_status to include current status; response includes liveViewUrl). |
get_camera_access_points | List access points associated with a camera. |
get_video_clips | Fetch clip URLs for a camera over a time window. args.camera_id, args.start_time, args.end_time (ISO 8601). Window must be < 24 h. |
get_users | List Brivo users. |
get_account | Fetch account metadata. |
set_refresh_token | Paste a refresh_token obtained out-of-band. |
authorize_code | Exchange an authorization code + redirect_uri for a refresh_token. |
register_subscription | Create the Brivo event-subscription webhook. |
unregister_subscription | Delete the webhook subscription. |
list_subscriptions | List all event subscriptions on the account. |
Cameras and video
Brivo exposes its connected cameras (Eagle Eye, OnAir, etc.) via the /cameras and /cameras/{id}/video endpoints. The driver leverages this to enrich GEM's Access Log:
- The
snapshotcommand builds an in-memoryaccessPoint → cameramap (cached for 5 minutes) by walking every camera and listing its access points. - When an access event fires, the existing access_log capture chain calls
dev.command({name: 'snapshot'}). The Brivo driver uses the most recent event's access-point id (set duringhandleAccessEvent) to find the right camera, then fetches a recent clip via/cameras/{id}/video?startTime=…&endTime=…(default 30 s lookback). - The clip URL is stored on the access_log row's
snapshotcolumn as a plainhttps://…string (image-based drivers continue to storedata:image/…;base64,…). The Access Log modal detects URLs vs. inline data, and known video extensions (.mp4,.webm,.mov,.m4v,.m3u8,.ts), and renders an embedded<video>player with autoplay/controls instead of an<img>.
To use a specific camera with a specific access point in macros (e.g. when you want a clip from a non-default camera), call get_video_clips directly with the desired camera_id and time window.
Related
- Access Control — rules, lockdown, duress, holiday calendars
- Users — credential lifecycle and duress PIN