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.

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.

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.

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 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_log row's snapshot column as a plain https://… string (image-based drivers continue to store data: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.

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