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. -
Set Elevated on the device (Devices). Brivo is the building's access-control system; the flag keeps non-elevated sessions (guest tablets, kiosks, PIN wall panels) off it. The driver refuses its credential, webhook, cardholder and camera commands for those callers regardless (see Commands), but Elevated is what withholds the door commands too.
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. The token is validated against Brivo before it is stored — a rejected token leaves the working credential untouched instead of overwriting it, so a bad paste cannot break door control until the next OAuth dance.
TLS verification
TLS certificates are verified on every Brivo call, including the token and auth-code exchanges — the requests that carry the client_id/client_secret, the api-key, and (under the password grant) the admin username and password. Brivo's hosts have valid CA certificates, so nothing needs configuring. The per-device allow_insecure_tls attribute (boolean, default off) exists only for a sandbox host override with a self-signed certificate; any value other than true still verifies.
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. |
set_refresh_token, authorize_code, the three subscription commands, get_users, get_account, and the camera / clip commands are refused with not authorized for a non-elevated caller, whatever the device's Elevated flag says — they can repoint or destroy the working credential, silently stop door events (which stops access logging and every access-control rule), or return the full cardholder directory and signed camera-clip URLs. unlock / open / pulse and snapshot are deliberately not gated here: they are the end-user actuation surface, confined by the device/zone Elevated flags and the central address gate, so wall panels that open doors keep working. Internal callers — macros, triggers, schedules — are unaffected.
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 Activity:
- 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 event's
snapshotcolumn as a plainhttps://…string (image-based drivers continue to storedata:image/…;base64,…). The Access Activity View Event dialog detects URLs vs. inline data, and known video extensions (.mp4,.webm,.mov,.m4v,.m3u8,.ts), and renders an embedded video player instead of an image.
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