Subscribing To Events

Overview

Real-time event streaming is a core capability of the acre Access Control platform. Every action in the system—from card reads and door status changes to user modifications and system alerts—generates events that are instantly published via MQTT, enabling you to build responsive, data-driven applications.

Why MQTT for Access Control?

acre Access Control uses MQTT (Message Queuing Telemetry Transport) as the foundation of our event distribution platform. MQTT is an industry-standard, lightweight publish/subscribe protocol designed for:

  • Real-time delivery - Events arrive as they happen
  • Efficiency - Minimal bandwidth usage, ideal for high-volume access control systems
  • Reliability - Built-in Quality of Service (QoS) levels ensure message delivery
  • Scalability - Handle thousands of simultaneous event subscribers without degradation
  • Firewall-friendly - WebSocket transport (WSS) works through standard HTTPS ports

Migration Note: acre Access Control migrated from SignalR to MQTT to provide better scalability, cross-platform support, and industry-standard integration patterns.

Endpoints

Production Endpoints

Region API Base URL MQTT Event Endpoint
United States https://api.us.acresecurity.cloud wss://events.us.acresecurity.cloud/mqtt
Canada https://api.ca.acresecurity.cloud wss://events.ca.acresecurity.cloud/mqtt
Europe https://api.eu.acresecurity.cloud wss://events.eu.acresecurity.cloud/mqtt

Dynamic Endpoint Discovery

Best Practice: Always retrieve the MQTT endpoint dynamically from the API’s SysInfo endpoint rather than hardcoding URLs:

var sysInfo = await client.GetSysInfo();
var mqttEndpoint = sysInfo.EventPublisherUrl; // e.g., "wss://events.us.acresecurity.cloud"
var mqttUrl = $"{mqttEndpoint}/mqtt";
# Using cURL and jq
MQTT_ENDPOINT=$(curl -s https://api.us.acresecurity.cloud/api/sysinfo | jq -r '.EventPublisherUrl')
echo "MQTT Endpoint: ${MQTT_ENDPOINT}/mqtt"

Why dynamic discovery?

  • Handles routing automatically
  • Accounts for failover scenarios
  • Future-proofs against endpoint changes

Authentication & Authorization

How MQTT Authentication Works

MQTT authentication for acre Access Control uses OAuth 2.0 Bearer tokens. Here’s the flow:

  1. Obtain an access token via the REST API /token endpoint
  2. Connect to MQTT using the access token as your username (password can be empty)
  3. Subscribe to topics - The server validates your token and instance permissions
  4. Receive events - Only events you’re authorized to see are delivered

Security Model

  • Instance-scoped: Your access token determines which instance(s) you can access
  • Topic-level authorization: The server checks every subscription request against your token
  • Automatic expiration: When your token expires (24 hours), you must reconnect with a fresh token
  • TLS encryption: All connections use WSS (WebSocket Secure) for encryption in transit

Topic Structure & Subscription Patterns

Events are published on a small, well-defined topic grammar with up to four levels:

/{folder}/{object}/{eventType}/{band}

The same event is published on several topic shapes simultaneously so that different subscription styles — by folder, by specific object, by event type, by priority band — all work.

A single MQTT SUBSCRIBE packet can carry multiple topic filters, and a client can issue more SUBSCRIBE packets over the life of the connection. So a subscriber composes whatever set of filters covers their use cases — for example, one filter per band to express “priority ≥ N” (see Subscribing to “Priority ≥ N”), or a banded firehose filter plus a targeted per-object filter for a specific reader.

Avoid overlapping filters for the same event. The broker delivers one copy per matching filter, so if two of your filters both match an event, your client receives two deliveries of it. Pick the most specific shape for each use case rather than layering broad and narrow filters that overlap.

All event messages are delivered at QoS 2 (ExactlyOnce) and are not retained.

Topic Levels

Level Segment Description Allowed values
1 {folder} Your instance identifier, or a child folder of it. 24-hex ObjectId
2 {object} A specific object the event references (a reader, person, app, etc.), or the literal $ meaning “no specific object — one delivery per event”. 24-hex ObjectId, or $
3 {eventType} The event type identifier (e.g., a specific alarm or access-granted definition). 24-hex ObjectId, or + (any event type)
4 {band} The priority band the event falls into. Omitted in sub-band mode (see Priority Bands). 1–3 digit threshold (e.g., 50, 100, 200, 255), or + (any band)

Wildcards. + is the standard MQTT single-level wildcard. # is the multi-level wildcard and must be the final segment of a filter. $ is a literal character at the object level — not a wildcard — meaning “no specific object”. Note that + does not match the literal $; use $ explicitly when you want firehose semantics.

Available Topic Shapes

Pick the shape that matches what you want. Most integrations want exactly-once delivery via a $-based filter.

Topic Pattern Description Deliveries per Event
/{folder}/# Everything in the folder. Includes legacy per-object and firehose copies — see note below. many
/{folder}/$/{eventType}/{band} Canonical pattern. One delivery per event of a given event type at a given priority band. 1
/{folder}/$/{eventType}/+ One delivery per event of a given event type at any priority band. 1
/{folder}/$/+/{band} One delivery per event at a given priority band, across all event types. 1
/{folder}/$/{eventType} One delivery per event of a given event type in sub-band mode (when the event’s priority is below the lowest configured band). 1
/{folder}/{objectId}/{eventType}/{band} Targeted: events on a specific object (e.g., a specific door reader) at a specific event type and band. 1 (if the event involves that object)
serverclock Server time heartbeat. Public — no authentication required. every 15 s

Picking the Right Shape

  • Use $ at the object level for once-per-event delivery. This is the right pattern for almost all integrations.
  • Use an explicit ObjectId at the object level when you want to target events involving a specific object (e.g., a specific reader or person).
  • Use + at the event-type or band level to wildcard that dimension.
  • Use # only as a terminal “give me everything below this point” wildcard.

About /{folder}/#

A # subscription receives multiple deliveries per event because each event is also published on additional legacy shapes (per-object, folder firehose) kept for backwards compatibility with older integrations. If you do not need backwards compatibility, prefer /{folder}/$/+/+ (or a more specific $-based filter) so that you receive exactly one delivery per event.

Topic Examples

# Once-per-event firehose for an instance, any event type, any band
/69705d500000000000000000/$/+/+

# All events of one event type (e.g., access granted "61cb71500000000000000000"),
# once per event, at any priority band
/69705d500000000000000000/$/61cb71500000000000000000/+

# All events at priority band 200 across every event type, once per event
/69705d500000000000000000/$/+/200

# Targeted: events on a specific reader, for one event type, at any band
/69705d500000000000000000/65aca4d00000000000000000/61cb71500000000000000000/+

# Sub-band events of one event type (priority below the lowest configured band)
/69705d500000000000000000/$/61cb71500000000000000000

# Legacy folder firehose — every event in the instance, once per event
/69705d500000000000000000/$

# Legacy per-object — events involving a specific person
/69705d500000000000000000/678f29d00000000000000000

# Server clock (no authentication required)
serverclock

Multi-Tenant & Enterprise Scenarios

For VAR (Value-Added Reseller) or Enterprise instances:

  • Subscribing at a parent folder (e.g., /{PARENT_INSTANCE_KEY}/$/+/+) automatically includes events from all child instances
  • Shared instances publish events to both their own folder and parent folders
  • This enables centralized monitoring across customer hierarchies

Priority Bands

A band is a tenant-configurable integer threshold that controls which {band} segment is placed on each event’s topic. Bands let subscribers ask for “priority ≥ N” semantics without the broker needing a comparison operator.

Bands are configured per-tenant as AlarmPriorityDefault objects scoped to your instance. A typical configuration is [50, 100, 200, 255].

Band-Selection Rule

For an event with priority P, the publisher selects the largest configured threshold less than or equal to P (the “closest lower band”). If P is below the lowest configured threshold, the event is published in sub-band mode — a 3-segment topic with no {band} segment.

Example — with configured thresholds [50, 100, 200, 255]:

Event Priority Band Selected Banded Topic Suffix
20 (sub-band) /{eventType}
50 50 /{eventType}/50
110 100 /{eventType}/100
200 200 /{eventType}/200
255 255 /{eventType}/255

So an event with Priority = 110 is published at band 100 (the closest configured threshold not exceeding 110). An event with Priority = 20 is below the lowest threshold (50) and is published in sub-band mode — its banded topics omit the trailing /{band} segment.

Discovering your bands. Query your tenant’s AlarmPriorityDefault objects through the Keep WebApi to learn which {band} values you can subscribe to. Band changes take effect on the publisher within a few minutes.

Subscribing to “Priority ≥ N”

MQTT does not have a comparison operator, so to subscribe to “priority ≥ N” you send one filter per qualifying band in a single SUBSCRIBE packet. For example, with thresholds [50, 100, 200, 255] and N = 100:

/{folder}/$/+/100
/{folder}/$/+/200
/{folder}/$/+/255

Each filter is treated independently by the broker. The result is one delivery per event whose priority falls into any of the listed bands.


Worked Example

Suppose your tenant has configured priority bands [50, 100, 200, 255], and a door reader fires an event with these attributes:

  • Priority: 137 → falls into band 100
  • Folder: 5fa8…aa
  • Event type: eee5…ff
  • Related objects: the reader (bbb1…cc), the person involved (ccc2…dd), the publishing app (aaa0…bb)

The broker publishes this event on several topics, including:

/5fa8…aa/$/eee5…ff/100              ← once-per-event banded firehose (canonical)
/5fa8…aa/bbb1…cc/eee5…ff/100        ← targeted to the reader
/5fa8…aa/ccc2…dd/eee5…ff/100        ← targeted to the person
/5fa8…aa/aaa0…bb/eee5…ff/100        ← targeted to the publishing app
/5fa8…aa/$                          ← legacy folder firehose
/5fa8…aa/bbb1…cc                    ← legacy per-object (reader)

What different subscribers see:

Filter Receives
/5fa8…aa/$/eee5…ff/100 Exactly one delivery for this event.
/5fa8…aa/$/+/100 Exactly one delivery for this event (any event type at band 100).
/5fa8…aa/bbb1…cc/eee5…ff/100 One delivery for this event, because it involves reader bbb1…cc.
/5fa8…aa/# Multiple deliveries for this same event (firehose + per-object + banded copies).

If the same event had fired with priority 10 (below the lowest band), the published topics would have no {band} segment — e.g., /5fa8…aa/$/eee5…ff. A filter of /5fa8…aa/$/eee5…ff would receive it; a filter of /5fa8…aa/$/eee5…ff/100 would not.


Complete Implementation Examples

C# Example: Subscribe to All Instance Events

This comprehensive example shows production-ready MQTT integration with error handling, automatic reconnection, and event deserialization.

Prerequisites

Install the required NuGet packages:

dotnet add package MQTTnet
dotnet add package MQTTnet.Extensions.ManagedClient
dotnet add package MongoDB.Bson
dotnet add package Feenics.Keep.WebApi.Model.Standard
dotnet add package Feenics.Keep.WebApi.Wrapper.Standard

Full Working Example

using System;
using System.Threading.Tasks;
using Feenics.Keep.WebApi.Wrapper;
using Feenics.Keep.WebApi.Model;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
using MongoDB.Bson.Serialization;

class EventSubscriber
{
    static async Task Main(string[] args)
    {
        // Configuration
        const string baseUrl = "https://api.us.acresecurity.cloud";
        const string instance = "QuickStart";
        const string username = "admin";
        const string password = "your-password";
        
        // 1. Login to API
        var client = new Client(baseUrl, userAgent: "EventMonitor/1.0");
        var (isLoggedIn, _, _) = await client.LoginAsync(instance, username, password);
        
        if (!isLoggedIn)
        {
            Console.WriteLine("❌ Login failed");
            return;
        }
        
        var currentInstance = await client.GetCurrentInstanceAsync();
        Console.WriteLine($"✓ Logged in to instance: {currentInstance.CommonName}");
        
        // 2. Get MQTT endpoint dynamically
        var sysInfo = await client.GetSysInfo();
        var mqttEndpoint = $"{sysInfo.EventPublisherUrl}/mqtt";
        Console.WriteLine($"✓ MQTT Endpoint: {mqttEndpoint}");
        
        // 3. Configure MQTT client
        var options = new ManagedMqttClientOptionsBuilder()
            .WithAutoReconnectDelay(TimeSpan.FromSeconds(5))
            .WithClientOptions(new MqttClientOptionsBuilder()
                .WithCommunicationTimeout(TimeSpan.FromSeconds(5))
                .WithKeepAlivePeriod(TimeSpan.FromSeconds(7.5))
                .WithCredentials(client.TokenResponse.access_token, "")
                .WithWebSocketServer(mqttEndpoint)
                .Build())
            .Build();
        
        // 4. Create and configure event handlers
        var eventsClient = new MqttFactory().CreateManagedMqttClient();
        
        eventsClient.UseConnectedHandler(async e =>
        {
            Console.WriteLine("✓ MQTT Connected");
            
            // Subscribe to all events in this instance
            await eventsClient.SubscribeAsync(new TopicFilterBuilder()
                .WithTopic($"/{currentInstance.Key}/$")
                .Build());
            
            Console.WriteLine($"✓ Subscribed to: /{currentInstance.Key}/$");
        });
        
        eventsClient.UseDisconnectedHandler(e =>
        {
            Console.WriteLine($"⚠️  MQTT Disconnected. Was connected: {e.ClientWasConnected}");
            if (e.Exception != null)
            {
                Console.WriteLine($"   Reason: {e.Exception.Message}");
            }
        });
        
        eventsClient.UseApplicationMessageReceivedHandler(async e =>
        {
            try
            {
                // Deserialize the event message
                var message = BsonSerializer.Deserialize<EventMessageData>(e.ApplicationMessage.Payload);
                
                // Display event details
                Console.WriteLine($"\n📨 Event Received:");
                Console.WriteLine($"   Key:        {message.Key}");
                Console.WriteLine($"   Type:       {message.EventTypeKey}");
                Console.WriteLine($"   Time:       {message.OccurredOn:yyyy-MM-dd HH:mm:ss} UTC");
                Console.WriteLine($"   Priority:   {message.Priority}");
                Console.WriteLine($"   Message:    {message.MessageLong}");
                
                // Optional: Deserialize event-specific data
                if (!string.IsNullOrEmpty(message.EventDataBsonBase64))
                {
                    var eventData = Convert.FromBase64String(message.EventDataBsonBase64);
                    // Process event-specific data based on EventTypeKey
                }
                
                // Display object links (what objects are involved)
                if (message.ObjectLinks?.Length > 0)
                {
                    Console.WriteLine($"   Objects:");
                    foreach (var link in message.ObjectLinks)
                    {
                        Console.WriteLine($"     - {link.Moniker}: {link.CommonName} ({link.Key})");
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Error processing message: {ex.Message}");
            }
        });
        
        // 5. Start MQTT client
        await eventsClient.StartAsync(options);
        Console.WriteLine("✓ MQTT Client Started");
        
        // 6. Keep running (press Ctrl+C to exit)
        Console.WriteLine("\nListening for events... (Press Ctrl+C to exit)\n");
        await Task.Delay(Timeout.Infinite);
    }
}

Expected Output

✓ Logged in to instance: QuickStart Demo
✓ MQTT Endpoint: wss://events.us.acresecurity.cloud/mqtt
✓ MQTT Connected
✓ Subscribed to: /69705d500000000000000000/$
✓ MQTT Client Started

Listening for events... (Press Ctrl+C to exit)

📨 Event Received:
   Key:        637801234567890123
   Type:       63cb71500000000000000000
   Time:       2026-01-21 18:46:58 UTC
   Priority:   200
   Message:    Access Granted: John Smith at Front Door
   Objects:
     - Person: John Smith (678f29d00000000000000000)
     - Reader: Front Door Reader (65aca4d00000000000000000)

C# Example: Monitor Server Clock (Heartbeat)

This simpler example shows how to use the public serverclock topic for connection monitoring.

using MQTTnet;
using MQTTnet.Client;
using MongoDB.Bson.Serialization;

class ServerClockMonitor
{
    static async Task Main(string[] args)
    {
        const string mqttEndpoint = "wss://events.us.acresecurity.cloud/mqtt";
        
        var mqttClient = new MqttFactory().CreateMqttClient();
        
        var options = new MqttClientOptionsBuilder()
            .WithWebSocketServer(mqttEndpoint)
            .Build();
        
        mqttClient.ApplicationMessageReceivedAsync += async e =>
        {
            var clockData = BsonSerializer.Deserialize<ServerClockMessage>(e.ApplicationMessage.Payload);
            Console.WriteLine($"Server Time: {clockData.CurrentTime:HH:mm:ss} UTC");
        };
        
        await mqttClient.ConnectAsync(options);
        
        // Subscribe to the public serverclock topic (no authentication required)
        await mqttClient.SubscribeAsync("serverclock");
        Console.WriteLine("Subscribed to serverclock - receiving heartbeats...\n");
        
        await Task.Delay(Timeout.Infinite);
    }
}

// Define the server clock message structure
public class ServerClockMessage
{
    public DateTime CurrentTime { get; set; }
}

cURL Example: Test MQTT Connection

While cURL isn’t used directly for WebSocket/MQTT connections, you can verify the MQTT endpoint is available and retrieve connection information:

# Get MQTT endpoint from SysInfo
ACCESS_TOKEN="your-access-token"
BASE_URL="https://api.us.acresecurity.cloud"

curl -s "${BASE_URL}/api/sysinfo" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.EventPublisherUrl'
# Output: "wss://events.us.acresecurity.cloud"

For testing MQTT connections, use tools like mosquitto_sub or MQTT Explorer:

# Using mosquitto_sub (install via: brew install mosquitto)
mosquitto_sub -h events.us.acresecurity.cloud \
  -p 443 \
  -t "/YOUR_INSTANCE_KEY/$" \
  -u "$ACCESS_TOKEN" \
  -P "" \
  --capath /etc/ssl/certs \
  -d

Event Message Structure

Every event message contains a consistent structure:

Field Type Description
Key string Unique identifier for this event
EventTypeKey string Type of event (matches EventTypeInfo.Key)
OccurredOn DateTime When the event occurred (UTC)
Priority int Event severity (0-1000, lower = more critical)
MessageShort string Brief event description
MessageLong string Detailed event description
ObjectLinks array Objects involved in the event
EventDataBsonBase64 string Base64-encoded BSON with event-specific data

Common Event Types

Event Description Priority Range
Access Granted Successful card read 200-300
Access Denied Failed access attempt 100-200
Door Forced Open Door opened without valid credential 50-100
Door Held Open Door open beyond timeout 100-150
Input Activation Alarm or sensor triggered 50-200
Person Modified User record updated 300-400
System Alert Controller communication issue 0-100

Best Practices

Practice Description
Use dynamic endpoint discovery Always retrieve MQTT URL from SysInfo, don’t hardcode
Implement reconnection logic Use ManagedMqttClient for automatic reconnection
Handle token expiration Tokens expire after 24 hours; implement refresh logic
Subscribe efficiently Use specific object topics when possible instead of /$ wildcard
Process events asynchronously Don’t block the message handler with long operations
Log connection status Track connect/disconnect events for troubleshooting
Use QoS appropriately QoS 1 (at least once) for important events
Implement backpressure Queue events if processing can’t keep up

Troubleshooting

Issue Possible Cause Solution
Connection refused Invalid endpoint URL Use SysInfo to get correct endpoint
Authentication failed Expired or invalid token Refresh access token and reconnect
No events received Wrong topic pattern Verify instance key and topic format
Connection drops Network instability Use ManagedMqttClient for auto-reconnect
Events delayed High server load Check system status; events are eventually consistent
Missing events Topic filter too specific Use /$ wildcard to receive all events
Token rejected Wrong region Ensure token was obtained from same region as MQTT endpoint

Additional Resources

TypeScript Example: Check out our example on GitHub: https://github.com/acresecurity/keep-MQTTExample