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.
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:
Migration Note: acre Access Control migrated from SignalR to MQTT to provide better scalability, cross-platform support, and industry-standard integration patterns.
| 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 |
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?
MQTT authentication for acre Access Control uses OAuth 2.0 Bearer tokens. Here’s the flow:
/token endpointEvents 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.
| 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.
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 |
$ at the object level for once-per-event delivery. This is the right pattern for almost all integrations.+ at the event-type or band level to wildcard that dimension.# only as a terminal “give me everything below this point” wildcard./{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.
# 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
For VAR (Value-Added Reseller) or Enterprise instances:
/{PARENT_INSTANCE_KEY}/$/+/+) automatically includes events from all child instancesA 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].
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
AlarmPriorityDefaultobjects through the Keep WebApi to learn which{band}values you can subscribe to. Band changes take effect on the publisher within a few minutes.
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.
Suppose your tenant has configured priority bands [50, 100, 200, 255], and a door reader fires an event with these attributes:
137 → falls into band 1005fa8…aaeee5…ffbbb1…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.
This comprehensive example shows production-ready MQTT integration with error handling, automatic reconnection, and event deserialization.
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
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);
}
}
✓ 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)
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; }
}
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
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 |
| 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 |
| 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 |
| 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 |
TypeScript Example: Check out our example on GitHub: https://github.com/acresecurity/keep-MQTTExample