This guide walks you through implementing a real-time notification system that displays alerts (weather notifications, school closures, etc.) on an external website. The system uses acre Access Control’s NotificationInfo objects and MQTT event streaming to instantly display notifications as they are created—without polling.
┌─────────────────────┐ ┌──────────────────────────┐ ┌─────────────────────┐
│ acre Application │ │ acre API & MQTT │ │ External Website │
│ │ │ │ │ │
│ Admin creates │──────▶│ 1. Stores Notification │ │ │
│ NotificationInfo │ │ 2. Publishes ObjectMod │──────▶│ Displays alert │
│ with Start/End │ │ event via MQTT │ │ in real-time │
│ dates │ │ │ │ │
└─────────────────────┘ └──────────────────────────┘ └─────────────────────┘
NotificationInfo in the acre application with a description (the message), start date, and end dateNotificationInfo objects, and displays the notification to users| Item | Description | Example |
|---|---|---|
| API Base URL | Your region’s API endpoint | https://api.us.acresecurity.cloud |
| Username | Service account username | notifications-service@yourcompany.com |
| Password | Service account password | •••••••• |
| Instance | The instance/folder for notifications | YourCompanyInstance |
C# (.NET 8+)
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
JavaScript/TypeScript
npm install mqtt
# For browser: mqtt.js works natively with WebSocket
Other frameworks and platforms will have their own requirements
The NotificationInfo object contains the notification data you’ll display:
| Property | Type | Description |
|---|---|---|
Key |
string |
Unique identifier for the notification |
CommonName |
string |
Short title/name of the notification |
Description |
string |
The notification message to display |
Priority |
int |
Priority level (lower = higher priority, default: 25) |
StartDate |
DateTime? |
When the notification becomes active |
EndDate |
DateTime? |
When the notification expires |
IsMarkdown |
bool |
Whether the Description contains Markdown |
IncludeChildInstances |
bool |
Whether to show in child instances |
{
"CommonName": "Weather Alert",
"Description": "School closures due to severe weather. All facilities closed until further notice.",
"Priority": 10,
"StartDate": "2026-01-23T08:00:00Z",
"EndDate": "2026-01-24T18:00:00Z",
"IsMarkdown": false,
"IncludeChildInstances": true
}
First, authenticate with the API to obtain an access token.
C# Example
using Feenics.Keep.WebApi.Wrapper;
public class NotificationService
{
private Client _client;
private readonly string _apiUrl = "https://api.us.acresecurity.cloud";
private readonly string _username = "{{YOUR_SERVICE_ACCOUNT_USERNAME}}";
private readonly string _password = "{{YOUR_SERVICE_ACCOUNT_PASSWORD}}";
private readonly string _instanceName = "{{YOUR_INSTANCE_NAME}}";
public async Task<bool> AuthenticateAsync()
{
_client = new Client(_apiUrl, null, "CHSNotificationDisplayService/1.0");
var (success, message) = await _client.LoginAsync(
_instanceName,
_username,
_password
);
if (success)
{
Console.WriteLine($"✓ Authenticated successfully");
Console.WriteLine($" Token expires: {_client.TokenResponse.ExpiresUtc}");
return true;
}
Console.WriteLine($"✗ Authentication failed: {message}");
return false;
}
// Access token for MQTT authentication
public string AccessToken => _client.TokenResponse.access_token;
// Get instance key from current instance
public async Task<string> GetInstanceKeyAsync()
{
var instance = await _client.GetCurrentInstanceAsync();
return instance.Key;
}
}
JavaScript Example
class NotificationService {
constructor() {
this.apiUrl = 'https://api.us.acresecurity.cloud';
this.username = '{{YOUR_SERVICE_ACCOUNT_USERNAME}}';
this.password = '{{YOUR_SERVICE_ACCOUNT_PASSWORD}}';
this.instanceName = '{{YOUR_INSTANCE_NAME}}';
this.tokenResponse = null;
this.instanceKey = null;
}
async authenticate() {
// Note: The /token endpoint expects username in format: instance\username
const response = await fetch(`${this.apiUrl}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'password',
client_id: 'consoleApp',
client_secret: 'consoleSecret',
username: `${this.instanceName}\\${this.username}`,
password: this.password,
instance: this.instanceName
})
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.status}`);
}
this.tokenResponse = await response.json();
console.log('✓ Authenticated successfully');
console.log(` Token expires in: ${this.tokenResponse.expires_in} seconds`);
// Fetch the instance key from the API
await this.fetchInstanceKey();
return true;
}
async fetchInstanceKey() {
const response = await fetch(`${this.apiUrl}/api`, {
headers: {
'Authorization': `Bearer ${this.tokenResponse.access_token}`
}
});
if (!response.ok) {
throw new Error(`Failed to get current instance: ${response.status}`);
}
const instance = await response.json();
this.instanceKey = instance.Key;
console.log(` Instance Key: ${this.instanceKey}`);
}
get accessToken() {
return this.tokenResponse?.access_token;
}
}
Access tokens expire after 24 hours, but you should refresh proactively every 6 hours to ensure uninterrupted service for long running applications and workflows.
C# Example - Token Refresh Loop
public class TokenRefreshService
{
private readonly Client _client;
private readonly string _instanceName;
private readonly string _username;
private readonly string _password;
private CancellationTokenSource _cts;
public event Action OnTokenRefreshed;
public async Task StartRefreshLoopAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
while (!_cts.Token.IsCancellationRequested)
{
try
{
// Wait 6 hours before refreshing
var refreshInterval = TimeSpan.FromHours(6);
// Calculate time until we should refresh
var tokenExpiry = _client.TokenResponse.ExpiresUtc;
var timeUntilExpiry = tokenExpiry - DateTime.UtcNow;
// Refresh if less than 6 hours remaining, or wait
if (timeUntilExpiry > refreshInterval)
{
var waitTime = timeUntilExpiry - refreshInterval;
Console.WriteLine($"Token refresh scheduled in {waitTime.TotalHours:F1} hours");
await Task.Delay(waitTime, _cts.Token);
}
// Attempt refresh
await RefreshTokenAsync();
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"Token refresh error: {ex.Message}");
// Wait 5 minutes before retry
await Task.Delay(TimeSpan.FromMinutes(5), _cts.Token);
}
}
}
private async Task RefreshTokenAsync()
{
try
{
await _client.RefreshAsync();
Console.WriteLine($"✓ Token refreshed. New expiry: {_client.TokenResponse.ExpiresUtc}");
OnTokenRefreshed?.Invoke();
}
catch (FailedOutcomeException ex) when (ex.HttpStatus == System.Net.HttpStatusCode.BadRequest)
{
// Refresh token expired - need full re-authentication
Console.WriteLine("Refresh token expired, re-authenticating...");
var (success, _) = await _client.LoginAsync(_instanceName, _username, _password);
if (success)
{
Console.WriteLine("✓ Re-authenticated successfully");
OnTokenRefreshed?.Invoke();
}
else
{
throw new Exception("Re-authentication failed");
}
}
}
}
JavaScript Example - Token Refresh
class TokenRefreshService {
constructor(notificationService) {
this.service = notificationService;
this.refreshIntervalMs = 6 * 60 * 60 * 1000; // 6 hours
this.refreshTimer = null;
this.onTokenRefreshed = null;
}
start() {
this.scheduleRefresh();
}
stop() {
if (this.refreshTimer) {
clearTimeout(this.refreshTimer);
this.refreshTimer = null;
}
}
scheduleRefresh() {
// Calculate time until 6 hours before expiry
const expiresIn = this.service.tokenResponse.expires_in * 1000;
const refreshIn = Math.max(expiresIn - this.refreshIntervalMs, 60000); // At least 1 minute
console.log(`Token refresh scheduled in ${(refreshIn / 3600000).toFixed(1)} hours`);
this.refreshTimer = setTimeout(() => this.refreshToken(), refreshIn);
}
async refreshToken() {
try {
const response = await fetch(`${this.service.apiUrl}/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: this.service.tokenResponse.refresh_token,
client_id: 'consoleApp',
client_secret: 'consoleSecret'
})
});
if (!response.ok) {
// Refresh failed - try full re-authentication
console.log('Refresh token expired, re-authenticating...');
await this.service.authenticate();
} else {
this.service.tokenResponse = await response.json();
console.log('✓ Token refreshed');
}
// Notify listeners (e.g., to reconnect MQTT)
if (this.onTokenRefreshed) {
this.onTokenRefreshed();
}
// Schedule next refresh
this.scheduleRefresh();
} catch (error) {
console.error('Token refresh failed:', error);
// Retry in 5 minutes
this.refreshTimer = setTimeout(() => this.refreshToken(), 5 * 60 * 1000);
}
}
}
The MQTT endpoint is dynamically retrieved from the API’s /api/sysinfo endpoint.
C# Example
public async Task<string> GetMqttEndpointAsync()
{
var sysInfo = await _client.GetSysInfo();
var mqttEndpoint = $"{sysInfo.EventPublisherUrl}/mqtt";
Console.WriteLine($"✓ MQTT Endpoint: {mqttEndpoint}");
return mqttEndpoint;
// Example: wss://events.us.acresecurity.cloud/mqtt
}
JavaScript Example
async getMqttEndpoint() {
const response = await fetch(`${this.apiUrl}/api/sysinfo`);
const sysInfo = await response.json();
const mqttEndpoint = `${sysInfo.EventPublisherUrl}/mqtt`;
console.log(`✓ MQTT Endpoint: ${mqttEndpoint}`);
return mqttEndpoint;
// Example: wss://events.us.acresecurity.cloud/mqtt
}
You need the Event Type Key for “Object Modified” events to subscribe to the correct MQTT topic. The Object Modified event type is identified by its moniker: Namespace: "KeepApiEvents", Nickname: "ObjectMod".
C# Example
public async Task<string> GetObjectModifiedEventKeyAsync()
{
var instance = await _client.GetCurrentInstanceAsync();
// Get the KeepApi app by its ApiKey
var keepApiApp = await _client.GetAppByApiKeyAsync(instance, "KeepApi");
// Find the Object Modified event type using its moniker
var objectModifiedEvent = await _client.GetEventTypeForAppByMonikerAsync(
keepApiApp,
new MonikerItem { Namespace = "KeepApiEvents", Nickname = "ObjectMod" }
);
if (objectModifiedEvent == null)
{
throw new Exception("Object Modified event type not found");
}
Console.WriteLine($"✓ Object Modified Event Key: {objectModifiedEvent.Key}");
return objectModifiedEvent.Key;
}
JavaScript Example
async getObjectModifiedEventKey() {
// Get all apps for the instance
const appsResponse = await fetch(
`${this.apiUrl}/api/f/${this.instanceKey}/apps`,
{ headers: { 'Authorization': `Bearer ${this.accessToken}` } }
);
const apps = await appsResponse.json();
// Find the KeepApi app by iterating through apps
const keepApiApp = apps.find(app => app.ApiKey === 'KeepApi');
if (!keepApiApp) {
throw new Error('KeepApi app not found');
}
// Get the Object Modified event type using its moniker
// The moniker for Object Modified is: Namespace="KeepApiEvents", Nickname="ObjectMod"
const eventTypeResponse = await fetch(
`${keepApiApp.Href}/eventtypes?namespace=KeepApiEvents&nickname=ObjectMod`,
{ headers: { 'Authorization': `Bearer ${this.accessToken}` } }
);
const objectModifiedEvent = await eventTypeResponse.json();
if (!objectModifiedEvent || !objectModifiedEvent.Key) {
throw new Error('Object Modified event type not found');
}
console.log(`✓ Object Modified Event Key: ${objectModifiedEvent.Key}`);
return objectModifiedEvent.Key;
}
Before subscribing to MQTT for real-time updates, you should fetch any existing active notifications that are currently valid. This ensures users see all relevant notifications when the service starts or reconnects.
The GetUnreadNotificationsForUserAsync method retrieves notifications that:
API Endpoint: GET /api/f/{folderId}/userreadnotifications/getunread
| Parameter | Description | Usage for Active Notifications |
|---|---|---|
afterStartDate |
Only include notifications where StartDate > this value | Usually not needed for active |
beforeStartDate |
Only include notifications where StartDate < this value | Set to DateTime.UtcNow to only show started notifications |
afterEndDate |
Only include notifications where EndDate > this value | Set to DateTime.UtcNow to exclude expired notifications |
beforeEndDate |
Only include notifications where EndDate < this value | Usually not needed for active |
skip |
Pagination offset | Optional |
limit |
Maximum results | Optional |
To get currently active notifications (started but not expired), use:
beforeStartDate: DateTime.UtcNow - Only notifications that have already startedafterEndDate: DateTime.UtcNow - Only notifications that haven’t expired yetC# Example
public async Task<IEnumerable<NotificationInfo>> GetActiveNotificationsAsync()
{
var instance = await _client.GetCurrentInstanceAsync();
var now = DateTime.UtcNow;
// Get unread notifications that are currently active:
// - StartDate is before now (already started)
// - EndDate is after now (not yet expired) OR EndDate is null (no expiry)
var activeNotifications = await _client.GetUnreadNotificationsForUserAsync(
folder: instance,
user: null, // Current user
beforeStartDate: now, // Only notifications that have started
beforeEndDate: null,
skip: null,
limit: null,
afterStartDate: null,
afterEndDate: now // Only notifications that haven't expired
);
Console.WriteLine($"✓ Loaded {activeNotifications.Count()} active notifications");
return activeNotifications;
}
JavaScript Example
async getActiveNotifications() {
const now = new Date().toISOString();
// Build query parameters for active notifications
const params = new URLSearchParams({
beforeStartDate: now, // Only notifications that have started
afterEndDate: now // Only notifications that haven't expired
});
const response = await fetch(
`${this.apiUrl}/api/f/${this.instanceKey}/userreadnotifications/getunread?${params}`,
{
headers: {
'Authorization': `Bearer ${this.accessToken}`
}
}
);
if (!response.ok) {
throw new Error(`Failed to get notifications: ${response.status}`);
}
const notifications = await response.json();
console.log(`✓ Loaded ${notifications.length} active notifications`);
return notifications;
}
Call this method when initializing and after MQTT reconnection to ensure no notifications are missed:
C# Example
// After connecting to MQTT, load initial notifications
var initialNotifications = await service.GetActiveNotificationsAsync();
foreach (var notification in initialNotifications)
{
DisplayNotification(notification, "ACTIVE");
}
JavaScript Example
// After MQTT connects, load initial notifications
mqttClient.on('connect', async () => {
// ... subscribe to topic ...
// Load existing active notifications
const activeNotifications = await getActiveNotifications();
for (const notification of activeNotifications) {
displayNotification(notification);
}
});
Connect to MQTT and subscribe to the Object Modified event topic.
C# Complete MQTT Subscriber
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Extensions.ManagedClient;
using MongoDB.Bson.Serialization;
using Feenics.Keep.WebApi.Model;
public class NotificationMqttSubscriber
{
private IManagedMqttClient _mqttClient;
private readonly string _instanceKey;
private readonly string _objectModifiedEventKey;
private readonly Func<string> _getAccessToken;
public event Action<NotificationInfo> OnNotificationCreated;
public event Action<NotificationInfo> OnNotificationUpdated;
public event Action<string> OnNotificationDeleted;
public NotificationMqttSubscriber(
string instanceKey,
string objectModifiedEventKey,
Func<string> getAccessToken)
{
_instanceKey = instanceKey;
_objectModifiedEventKey = objectModifiedEventKey;
_getAccessToken = getAccessToken;
}
public async Task ConnectAsync(string mqttEndpoint)
{
var options = new ManagedMqttClientOptionsBuilder()
.WithAutoReconnectDelay(TimeSpan.FromSeconds(5))
.WithClientOptions(new MqttClientOptionsBuilder()
.WithTimeout(TimeSpan.FromSeconds(10))
.WithKeepAlivePeriod(TimeSpan.FromSeconds(15))
.WithCredentials(_getAccessToken(), (string)null)
.WithWebSocketServer(o => o.WithUri(mqttEndpoint))
.Build())
.Build();
_mqttClient = new MqttFactory().CreateManagedMqttClient();
_mqttClient.ConnectedAsync += OnConnected;
_mqttClient.DisconnectedAsync += OnDisconnected;
_mqttClient.ApplicationMessageReceivedAsync += OnMessageReceived;
await _mqttClient.StartAsync(options);
// Subscribe to Object Modified events for this instance
var topic = $"/{_instanceKey}/{_objectModifiedEventKey}";
await _mqttClient.SubscribeAsync(new MqttTopicFilterBuilder()
.WithTopic(topic)
.Build());
Console.WriteLine($"✓ Subscribed to topic: {topic}");
}
private Task OnConnected(MqttClientConnectedEventArgs e)
{
Console.WriteLine("✓ Connected to MQTT broker");
return Task.CompletedTask;
}
private Task OnDisconnected(MqttClientDisconnectedEventArgs e)
{
Console.WriteLine($"⚠ Disconnected from MQTT: {e.Exception?.Message}");
return Task.CompletedTask;
}
private Task OnMessageReceived(MqttApplicationMessageReceivedEventArgs e)
{
try
{
var payload = e.ApplicationMessage.PayloadSegment.ToArray();
var eventMessage = BsonSerializer.Deserialize<EventMessageData>(payload);
// Verify this is an Object Modified event
if (eventMessage.EventTypeKey != _objectModifiedEventKey)
{
return Task.CompletedTask;
}
// Deserialize the ObjectMod event data
var objectModData = DeserializeObjectModEventData(eventMessage.EventDataBsonBase64);
// ═══════════════════════════════════════════════════════════════
// CRITICAL FILTER: Only process NotificationInfo objects
// ═══════════════════════════════════════════════════════════════
if (!objectModData.Types.Contains("Notification"))
{
// Not a notification - ignore this event
return Task.CompletedTask;
}
Console.WriteLine($"📢 Notification event received: {objectModData.Action}");
// Handle based on action type
switch (objectModData.Action)
{
case "POST": // New notification created
if (objectModData.AfterData is NotificationInfo newNotification)
{
Console.WriteLine($" ✓ New: {newNotification.CommonName}");
OnNotificationCreated?.Invoke(newNotification);
}
break;
case "PUT": // Notification updated
if (objectModData.AfterData is NotificationInfo updatedNotification)
{
Console.WriteLine($" ✓ Updated: {updatedNotification.CommonName}");
OnNotificationUpdated?.Invoke(updatedNotification);
}
break;
case "DELETE": // Notification deleted
var deletedKey = objectModData.Key;
Console.WriteLine($" ✓ Deleted: {deletedKey}");
OnNotificationDeleted?.Invoke(deletedKey);
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing message: {ex.Message}");
}
return Task.CompletedTask;
}
private ObjectModEventData DeserializeObjectModEventData(string eventDataBsonBase64)
{
// Handle both Base64-encoded BSON and raw JSON
try
{
var bytes = Convert.FromBase64String(eventDataBsonBase64);
return BsonSerializer.Deserialize<ObjectModEventData>(bytes);
}
catch
{
// Fallback to parsing as JSON/BSON document
var doc = MongoDB.Bson.BsonDocument.Parse(eventDataBsonBase64);
return BsonSerializer.Deserialize<ObjectModEventData>(doc);
}
}
public async Task DisconnectAsync()
{
if (_mqttClient != null)
{
await _mqttClient.StopAsync();
_mqttClient.Dispose();
}
}
}
JavaScript Complete MQTT Subscriber (Browser)
import mqtt from 'mqtt';
class NotificationMqttSubscriber {
constructor(instanceKey, objectModifiedEventKey, getAccessToken) {
this.instanceKey = instanceKey;
this.objectModifiedEventKey = objectModifiedEventKey;
this.getAccessToken = getAccessToken;
this.client = null;
// Event callbacks
this.onNotificationCreated = null;
this.onNotificationUpdated = null;
this.onNotificationDeleted = null;
}
connect(mqttEndpoint) {
const options = {
username: this.getAccessToken(),
password: '',
reconnectPeriod: 5000,
connectTimeout: 10000,
keepalive: 15
};
this.client = mqtt.connect(mqttEndpoint, options);
this.client.on('connect', () => {
console.log('✓ Connected to MQTT broker');
// Subscribe to Object Modified events
const topic = `/${this.instanceKey}/${this.objectModifiedEventKey}`;
this.client.subscribe(topic, (err) => {
if (err) {
console.error('Subscribe error:', err);
} else {
console.log(`✓ Subscribed to topic: ${topic}`);
}
});
});
this.client.on('disconnect', () => {
console.log('⚠ Disconnected from MQTT');
});
this.client.on('error', (error) => {
console.error('MQTT error:', error);
});
this.client.on('message', (topic, payload) => {
this.handleMessage(payload);
});
}
handleMessage(payload) {
try {
// For browser, payload arrives as Buffer - parse as JSON
// Note: In production, you may need BSON parsing library
const eventMessage = JSON.parse(payload.toString());
// Verify this is an Object Modified event
if (eventMessage.EventTypeKey !== this.objectModifiedEventKey) {
return;
}
// Parse the event data
const objectModData = this.parseObjectModData(eventMessage.EventDataBsonBase64);
// ═══════════════════════════════════════════════════════════════
// CRITICAL FILTER: Only process NotificationInfo objects
// ═══════════════════════════════════════════════════════════════
if (!objectModData.Types || !objectModData.Types.includes('Notification')) {
// Not a notification - ignore
return;
}
console.log(`📢 Notification event: ${objectModData.Action}`);
switch (objectModData.Action) {
case 'POST': // New notification
if (objectModData.AfterData && this.onNotificationCreated) {
console.log(` ✓ New: ${objectModData.AfterData.CommonName}`);
this.onNotificationCreated(objectModData.AfterData);
}
break;
case 'PUT': // Updated notification
if (objectModData.AfterData && this.onNotificationUpdated) {
console.log(` ✓ Updated: ${objectModData.AfterData.CommonName}`);
this.onNotificationUpdated(objectModData.AfterData);
}
break;
case 'DELETE': // Deleted notification
if (this.onNotificationDeleted) {
console.log(` ✓ Deleted: ${objectModData.Key}`);
this.onNotificationDeleted(objectModData.Key);
}
break;
}
} catch (error) {
console.error('Error processing message:', error);
}
}
parseObjectModData(eventDataBsonBase64) {
// For browser, you may receive JSON string or need BSON library
// This example assumes JSON encoding
try {
// Try Base64 decode first
const decoded = atob(eventDataBsonBase64);
return JSON.parse(decoded);
} catch {
// Fallback to direct JSON parse
return JSON.parse(eventDataBsonBase64);
}
}
disconnect() {
if (this.client) {
this.client.end();
this.client = null;
}
}
}
The Object Modified event fires for every object change in the system. You must filter carefully:
| Filter Criteria | Value | Why |
|---|---|---|
EventTypeKey |
Your Object Modified event key | Only Object Modified events |
Types array |
Contains "Notification" |
Only NotificationInfo objects |
Action |
"POST", "PUT", or "DELETE" |
Create, update, or delete actions |
Before displaying a notification, verify it’s currently active:
private bool IsNotificationActive(NotificationInfo notification)
{
var now = DateTime.UtcNow;
// Check start date (if specified)
if (notification.StartDate.HasValue && notification.StartDate.Value > now)
{
return false; // Not yet active
}
// Check end date (if specified)
if (notification.EndDate.HasValue && notification.EndDate.Value < now)
{
return false; // Expired
}
return true;
}
function isNotificationActive(notification) {
const now = new Date();
if (notification.StartDate && new Date(notification.StartDate) > now) {
return false; // Not yet active
}
if (notification.EndDate && new Date(notification.EndDate) < now) {
return false; // Expired
}
return true;
}
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
var cts = new CancellationTokenSource();
Console.CancelKeyPress += (s, e) => { e.Cancel = true; cts.Cancel(); };
// Configuration
const string apiUrl = "https://api.us.acresecurity.cloud";
const string username = "{{YOUR_SERVICE_ACCOUNT_USERNAME}}";
const string password = "{{YOUR_SERVICE_ACCOUNT_PASSWORD}}";
const string instanceName = "{{YOUR_INSTANCE_NAME}}";
Console.WriteLine("=== Notification Display Service ===\n");
// 1. Authenticate
var service = new NotificationService(apiUrl, username, password, instanceName);
if (!await service.AuthenticateAsync())
{
Console.WriteLine("Failed to authenticate. Exiting.");
return;
}
// 2. Get MQTT endpoint
var mqttEndpoint = await service.GetMqttEndpointAsync();
// 3. Get Object Modified event key - STATIC VALUE for API URL -> SHOULD BE SAVED OR CACHED
var objectModEventKey = await service.GetObjectModifiedEventKeyAsync();
// 4. Get instance key
var instanceKey = await service.GetInstanceKeyAsync();
// 5. Start token refresh loop
var tokenRefresh = new TokenRefreshService(service);
_ = tokenRefresh.StartRefreshLoopAsync(cts.Token);
// 6. Load initial active notifications
Console.WriteLine("\n--- Loading Active Notifications ---");
var activeNotifications = await service.GetActiveNotificationsAsync();
foreach (var notification in activeNotifications)
{
DisplayNotification(notification, "ACTIVE");
}
// 7. Connect to MQTT and subscribe
var subscriber = new NotificationMqttSubscriber(
instanceKey,
objectModEventKey,
() => service.AccessToken
);
// Handle notification events
subscriber.OnNotificationCreated += notification =>
{
if (IsNotificationActive(notification))
{
DisplayNotification(notification, "NEW");
}
};
subscriber.OnNotificationUpdated += notification =>
{
if (IsNotificationActive(notification))
{
DisplayNotification(notification, "UPDATED");
}
};
subscriber.OnNotificationDeleted += key =>
{
Console.WriteLine($"\n╔════════════════════════════════════════╗");
Console.WriteLine($"║ NOTIFICATION REMOVED ║");
Console.WriteLine($"║ Key: {key,-32} ║");
Console.WriteLine($"╚════════════════════════════════════════╝\n");
};
// Reconnect on token refresh
tokenRefresh.OnTokenRefreshed += async () =>
{
await subscriber.DisconnectAsync();
await subscriber.ConnectAsync(mqttEndpoint);
};
await subscriber.ConnectAsync(mqttEndpoint);
Console.WriteLine("\n✓ Listening for notifications... (Press Ctrl+C to exit)\n");
// Keep running
try
{
await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException) { }
await subscriber.DisconnectAsync();
Console.WriteLine("Service stopped.");
}
static bool IsNotificationActive(NotificationInfo notification)
{
var now = DateTime.UtcNow;
if (notification.StartDate.HasValue && notification.StartDate.Value > now) return false;
if (notification.EndDate.HasValue && notification.EndDate.Value < now) return false;
return true;
}
static void DisplayNotification(NotificationInfo notification, string status)
{
Console.WriteLine($"\n╔════════════════════════════════════════════════════════════╗");
Console.WriteLine($"║ 📢 {status} NOTIFICATION ");
Console.WriteLine($"╠════════════════════════════════════════════════════════════╣");
Console.WriteLine($"║ Title: {notification.CommonName}");
Console.WriteLine($"║ Priority: {notification.Priority}");
Console.WriteLine($"╟────────────────────────────────────────────────────────────╢");
Console.WriteLine($"║ {notification.Description}");
Console.WriteLine($"╟────────────────────────────────────────────────────────────╢");
Console.WriteLine($"║ Active: {notification.StartDate?.ToString("g") ?? "Immediately"}");
Console.WriteLine($"║ Until: {notification.EndDate?.ToString("g") ?? "No expiry"}");
Console.WriteLine($"╚════════════════════════════════════════════════════════════╝\n");
}
}
<!DOCTYPE html>
<html>
<head>
<title>Notification Display</title>
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
<style>
.notification-banner {
background: linear-gradient(135deg, #ff6b6b, #feca57);
color: white;
padding: 20px;
margin: 10px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
animation: slideIn 0.3s ease-out;
}
.notification-banner.priority-high { background: linear-gradient(135deg, #ff4757, #ff6b81); }
.notification-banner.priority-normal { background: linear-gradient(135deg, #1e90ff, #3742fa); }
.notification-title { font-size: 1.2em; font-weight: bold; margin-bottom: 8px; }
.notification-message { font-size: 1em; }
.notification-meta { font-size: 0.8em; opacity: 0.8; margin-top: 10px; }
@keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } }
#status { padding: 10px; margin: 10px; border-radius: 4px; }
#status.connected { background: #d4edda; color: #155724; }
#status.disconnected { background: #f8d7da; color: #721c24; }
</style>
</head>
<body>
<h1>🔔 Live Notifications</h1>
<div id="status" class="disconnected">Connecting...</div>
<div id="notifications"></div>
<script>
// Configuration - Replace with your values
const CONFIG = {
apiUrl: 'https://api.us.acresecurity.cloud',
username: '{{YOUR_SERVICE_ACCOUNT_USERNAME}}',
password: '{{YOUR_SERVICE_ACCOUNT_PASSWORD}}',
instanceName: '{{YOUR_INSTANCE_NAME}}'
};
let tokenResponse = null;
let mqttClient = null;
let objectModEventKey = null;
const activeNotifications = new Map();
async function initialize() {
try {
// 1. Authenticate
updateStatus('Authenticating...');
await authenticate();
// 2. Get MQTT endpoint
updateStatus('Getting MQTT endpoint...');
const mqttEndpoint = await getMqttEndpoint();
// 3. Get Object Modified event key
updateStatus('Getting event type...');
objectModEventKey = await getObjectModEventKey();
// 4. Load initial active notifications
updateStatus('Loading active notifications...');
await loadActiveNotifications();
// 5. Connect to MQTT
updateStatus('Connecting to MQTT...');
connectMqtt(mqttEndpoint);
// 6. Start token refresh
startTokenRefresh();
} catch (error) {
updateStatus(`Error: ${error.message}`, false);
console.error(error);
}
}
async function loadActiveNotifications() {
const now = new Date().toISOString();
const params = new URLSearchParams({
beforeStartDate: now, // Only notifications that have started
afterEndDate: now // Only notifications that haven't expired
});
const response = await fetch(
`${CONFIG.apiUrl}/api/f/${tokenResponse.instanceKey}/userreadnotifications/getunread?${params}`,
{
headers: {
'Authorization': `Bearer ${tokenResponse.access_token}`
}
}
);
if (!response.ok) {
console.error('Failed to load active notifications');
return;
}
const notifications = await response.json();
console.log(`Loaded ${notifications.length} active notifications`);
for (const notification of notifications) {
displayNotification(notification);
}
}
async function authenticate() {
// Note: The /token endpoint expects username in format: instance\username
const response = await fetch(`${CONFIG.apiUrl}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'password',
client_id: 'consoleApp',
client_secret: 'consoleSecret',
username: `${CONFIG.instanceName}\\${CONFIG.username}`,
password: CONFIG.password,
instance: CONFIG.instanceName
})
});
if (!response.ok) throw new Error('Authentication failed');
tokenResponse = await response.json();
// Get instance key from the API
const instanceResp = await fetch(`${CONFIG.apiUrl}/api`, {
headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` }
});
const instance = await instanceResp.json();
tokenResponse.instanceKey = instance.Key;
}
async function getMqttEndpoint() {
const response = await fetch(`${CONFIG.apiUrl}/api/sysinfo`);
const sysInfo = await response.json();
return `${sysInfo.EventPublisherUrl}/mqtt`;
}
async function getObjectModEventKey() {
// Get all apps for the instance
const appsResp = await fetch(
`${CONFIG.apiUrl}/api/f/${tokenResponse.instanceKey}/apps`,
{ headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` } }
);
const apps = await appsResp.json();
// Find KeepApi app by iterating through apps
const keepApiApp = apps.find(app => app.ApiKey === 'KeepApi');
if (!keepApiApp) throw new Error('KeepApi app not found');
// Get Object Modified event type using its moniker
// Moniker: Namespace="KeepApiEvents", Nickname="ObjectMod"
const eventResp = await fetch(
`${keepApiApp.Href}/eventtypes?namespace=KeepApiEvents&nickname=ObjectMod`,
{ headers: { 'Authorization': `Bearer ${tokenResponse.access_token}` } }
);
const objModEvent = await eventResp.json();
if (!objModEvent || !objModEvent.Key) throw new Error('Object Modified event not found');
return objModEvent.Key;
}
function connectMqtt(endpoint) {
mqttClient = mqtt.connect(endpoint, {
username: tokenResponse.access_token,
password: '',
reconnectPeriod: 5000
});
mqttClient.on('connect', () => {
updateStatus('Connected - Listening for notifications', true);
const topic = `/${tokenResponse.instanceKey}/${objectModEventKey}`;
mqttClient.subscribe(topic);
console.log('Subscribed to:', topic);
});
mqttClient.on('disconnect', () => {
updateStatus('Disconnected - Reconnecting...', false);
});
mqttClient.on('message', (topic, payload) => {
handleMessage(payload);
});
}
function handleMessage(payload) {
try {
const event = JSON.parse(payload.toString());
if (event.EventTypeKey !== objectModEventKey) return;
// Parse event data (simplified - may need BSON library for production)
let objectModData;
try {
objectModData = JSON.parse(atob(event.EventDataBsonBase64));
} catch {
objectModData = JSON.parse(event.EventDataBsonBase64);
}
// FILTER: Only NotificationInfo objects
if (!objectModData.Types?.includes('Notification')) return;
console.log('Notification event:', objectModData.Action);
switch (objectModData.Action) {
case 'POST':
case 'PUT':
if (objectModData.AfterData && isActive(objectModData.AfterData)) {
displayNotification(objectModData.AfterData);
}
break;
case 'DELETE':
removeNotification(objectModData.Key);
break;
}
} catch (e) {
console.error('Error handling message:', e);
}
}
function isActive(notification) {
const now = new Date();
if (notification.StartDate && new Date(notification.StartDate) > now) return false;
if (notification.EndDate && new Date(notification.EndDate) < now) return false;
return true;
}
function displayNotification(notification) {
activeNotifications.set(notification.Key, notification);
renderNotifications();
}
function removeNotification(key) {
activeNotifications.delete(key);
renderNotifications();
}
function renderNotifications() {
const container = document.getElementById('notifications');
container.innerHTML = '';
// Sort by priority (lower = more important)
const sorted = [...activeNotifications.values()].sort((a, b) => a.Priority - b.Priority);
for (const n of sorted) {
const div = document.createElement('div');
div.className = `notification-banner ${n.Priority <= 10 ? 'priority-high' : 'priority-normal'}`;
div.innerHTML = `
<div class="notification-title">${n.CommonName}</div>
<div class="notification-message">${n.Description}</div>
<div class="notification-meta">
Active: ${n.StartDate ? new Date(n.StartDate).toLocaleString() : 'Now'}
${n.EndDate ? ' - ' + new Date(n.EndDate).toLocaleString() : ''}
</div>
`;
container.appendChild(div);
}
}
function updateStatus(message, connected = null) {
const el = document.getElementById('status');
el.textContent = message;
if (connected !== null) {
el.className = connected ? 'connected' : 'disconnected';
}
}
function startTokenRefresh() {
// Refresh every 6 hours
setInterval(async () => {
try {
const response = await fetch(`${CONFIG.apiUrl}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: tokenResponse.refresh_token,
client_id: 'consoleApp',
client_secret: 'consoleSecret'
})
});
if (response.ok) {
tokenResponse = await response.json();
console.log('Token refreshed');
// Reconnect MQTT with new token
mqttClient.end();
connectMqtt(await getMqttEndpoint());
}
} catch (e) {
console.error('Token refresh failed:', e);
}
}, 6 * 60 * 60 * 1000); // 6 hours
}
// Start the application
initialize();
</script>
</body>
</html>
The ObjectModEventData object contains details about what was modified:
| Property | Type | Description |
|---|---|---|
Action |
string |
"POST" (create), "PUT" (update), "DELETE" (delete) |
Types |
string[] |
Object type(s) - filter for "Notification" |
Key |
string |
Object key (useful for delete events) |
CommonName |
string |
Object name |
BeforeData |
Item |
Object state before modification (null for POST) |
AfterData |
Item |
Object state after modification (null for DELETE) |
User |
ObjectLinkItem |
User who made the change |
// Only process Object Modified events
if (eventMessage.EventTypeKey != objectModifiedEventKey) return;
// Only process NotificationInfo objects
var objectModData = Deserialize(eventMessage.EventDataBsonBase64);
if (!objectModData.Types.Contains("Notification")) return;
// Handle the action
switch (objectModData.Action)
{
case "POST": // Created - show notification
case "PUT": // Updated - refresh notification
case "DELETE": // Removed - hide notification
}