Skip to main content

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 pin or rfid event 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 (with effectiveFrom / effectiveTo mirroring GEM's valid_from / valid_until lifecycle); cards are created as separate Brivo credential records and attached. When you disable, expire, or revoke a GEM user, the access control engine sets suspended=true on Brivo on the next sweep.
  • Pulses doors on demand via the unlock command — useful for macros that open a Brivo door without a credential event (e.g. "guest arriving" workflows).

Prerequisites

  1. A Brivo developer-portal application (client_id + client_secret).
  2. A per-installation Brivo api-key (issued by Brivo when you provision the integration).
  3. 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

  1. System > Devices: add a device with driver brivo. Fill in:

    • client_id
    • client_secret (secure)
    • api_key (secure)
    • either username + password (secure) for the password grant, or run the set_refresh_token command 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.
  2. Add zones under the device, one per Brivo access point you want to control. zone.address = the numeric Brivo access_point id. Run the get_access_points command to enumerate available doors.

  3. Register the webhook by running the register_subscription command. Brivo will POST events to {webhook_base_url}/driver_webhook/{device_id}/{token}. The token is generated automatically and stored as a secure attribute. The encryptedSecretWord Brivo returns is also stored for future signature verification.

  4. Reference the device in Access Control rules. Set access_device_id to the Brivo device. When Brivo sees a card swipe or PIN entry, the driver emits a pin / rfid event, GEM matches it to a user, and the rule fires its action.

  5. 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.

For production, do the auth-code dance once and feed the resulting refresh_token to GEM:

  1. Direct an admin browser to:
    https://auth.brivo.com/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={your_redirect}
  2. After approval Brivo redirects to {redirect_uri}?code=....
  3. Run the GEM command authorize_code with the code and the redirect_uri you 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 encryptedSecretWord at 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 schemasecurityAction, actor, eventObject, eventData.credentials[], eventData.unknownWeigand, etc. The driver classifies and routes them as follows:

Brivo event shapeGEM behavior
actor.objectType === 'USER' and the user was previously synced from GEMLook 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 GEMLogged 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 setEmit 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_POINTLogged 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_code or set_refresh_token.
  • The held_open_threshold_sec rule field is currently informational. Forced-door / held-open detection is on the roadmap.

Commands

CommandPurpose
unlock / open / pulsePulse a door. args.address = Brivo access_point id.
snapshotReturn 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_pointsList available access points (use to populate zone addresses).
get_camerasList all cameras on the Brivo account.
get_cameraRetrieve a single camera (set args.show_status to include current status; response includes liveViewUrl).
get_camera_access_pointsList access points associated with a camera.
get_video_clipsFetch 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_usersList Brivo users.
get_accountFetch account metadata.
set_refresh_tokenPaste a refresh_token obtained out-of-band.
authorize_codeExchange an authorization code + redirect_uri for a refresh_token.
register_subscriptionCreate the Brivo event-subscription webhook.
unregister_subscriptionDelete the webhook subscription.
list_subscriptionsList all event subscriptions on the account.
Commands that require an elevated session

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 snapshot command builds an in-memory accessPoint → camera map (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 during handleAccessEvent) 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 snapshot column as a plain https://… string (image-based drivers continue to store data: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.

  • Access Control — rules, lockdown, duress, holiday calendars
  • Users — credential lifecycle and duress PIN