Linking readers to access levels is the core operation that defines where cardholders can go. An access level contains one or more AccessLevelEntryItem objects, each specifying a reader and the schedule that controls when access is permitted through that reader.
This is the connection point that brings together:
| Property | Type | Description |
|---|---|---|
ReaderId |
string | The Key of the reader (MercuryReaderInfo.Key) |
ReaderCommonName |
string | Display name of the reader |
ScheduleId |
string | The Key of the schedule (ScheduleInfo.Key) |
ScheduleCommonName |
string | Display name of the schedule |
Before linking readers to access levels, ensure you have:
// Step 1: Get the existing access level
var accessLevel = (await client.SearchAsync(currentInstance, "{\"_t\": \"AccessLevel\", \"CommonName\":\"Standard Employee Access\"}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Step 2: Get the reader and schedule to link
var reader = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\", \"CommonName\":\"Main Entrance Reader\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var schedule = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"StandardBusinessHours\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Step 3: Create the entry linking reader + schedule
accessLevel.AccessLevelEntries = new AccessLevelEntryItem[]
{
new AccessLevelEntryItem
{
ReaderId = reader.Key,
ReaderCommonName = reader.CommonName,
ScheduleId = schedule.Key,
ScheduleCommonName = schedule.CommonName
}
};
// Step 4: Update the access level
await client.UpdateAccessLevelAsync(accessLevel);
Console.WriteLine($"Linked {reader.CommonName} to {accessLevel.CommonName} on schedule {schedule.CommonName}");
Create access levels that grant access to multiple doors:
// Get the access level
var accessLevel = (await client.SearchAsync(currentInstance, "{\"_t\": \"AccessLevel\", \"CommonName\":\"Engineering Department\"}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Get multiple readers
var mainEntrance = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\", \"CommonName\":\"Main Entrance\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var engineeringLab = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\", \"CommonName\":\"Engineering Lab\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var serverRoom = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\", \"CommonName\":\"Server Room\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
// Get schedules (may be different for each reader)
var businessHours = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"BusinessHours\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
var extendedHours = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"ExtendedHours\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
var alwaysSchedule = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"24-7-Access\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Link all readers with their respective schedules
accessLevel.AccessLevelEntries = new AccessLevelEntryItem[]
{
// Main entrance - business hours only
new AccessLevelEntryItem
{
ReaderId = mainEntrance.Key,
ReaderCommonName = mainEntrance.CommonName,
ScheduleId = businessHours.Key,
ScheduleCommonName = businessHours.CommonName
},
// Engineering lab - extended hours
new AccessLevelEntryItem
{
ReaderId = engineeringLab.Key,
ReaderCommonName = engineeringLab.CommonName,
ScheduleId = extendedHours.Key,
ScheduleCommonName = extendedHours.CommonName
},
// Server room - 24/7 access
new AccessLevelEntryItem
{
ReaderId = serverRoom.Key,
ReaderCommonName = serverRoom.CommonName,
ScheduleId = alwaysSchedule.Key,
ScheduleCommonName = alwaysSchedule.CommonName
}
};
await client.UpdateAccessLevelAsync(accessLevel);
Preserve existing reader links when adding new ones:
// Get the access level with existing entries
var accessLevel = (await client.SearchAsync(currentInstance, "{\"_t\": \"AccessLevel\", \"CommonName\":\"Standard Access\"}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Get new reader and schedule to add
var newReader = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\", \"CommonName\":\"New Entrance Reader\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var schedule = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"BusinessHours\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Combine existing entries with new entry
var existingEntries = accessLevel.AccessLevelEntries?.ToList() ?? new List<AccessLevelEntryItem>();
existingEntries.Add(new AccessLevelEntryItem
{
ReaderId = newReader.Key,
ReaderCommonName = newReader.CommonName,
ScheduleId = schedule.Key,
ScheduleCommonName = schedule.CommonName
});
accessLevel.AccessLevelEntries = existingEntries.ToArray();
await client.UpdateAccessLevelAsync(accessLevel);
// Get the access level
var accessLevel = (await client.SearchAsync(currentInstance, "{\"_t\": \"AccessLevel\", \"CommonName\":\"Standard Access\"}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Filter out the reader to remove
var readerToRemove = "Decommissioned Reader";
var updatedEntries = accessLevel.AccessLevelEntries
.Where(e => e.ReaderCommonName != readerToRemove)
.ToArray();
accessLevel.AccessLevelEntries = updatedEntries;
await client.UpdateAccessLevelAsync(accessLevel);
Create an “All Doors” access level:
// Get all readers in the instance
var allReaders = (await client.SearchAsync(currentInstance, "{\"_t\": \"MercuryReader\"}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
// Get the always-on schedule
var alwaysSchedule = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"24-7-Access\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Create or get the access level
var allDoorsAccess = await client.AddAccessLevelAsync(currentInstance,
new AccessLevelInfo
{
CommonName = "All Doors Access",
AccessLevelEntries = allReaders.Select(reader => new AccessLevelEntryItem
{
ReaderId = reader.Key,
ReaderCommonName = reader.CommonName,
ScheduleId = alwaysSchedule.Key,
ScheduleCommonName = alwaysSchedule.CommonName
}).ToArray()
});
Console.WriteLine($"Created access level with {allReaders.Count()} readers");
curl -X PUT \
"https://api.us.acresecurity.cloud/api/f/INSTANCE_KEY/accesslevels/ACCESS_LEVEL_KEY" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"$type": "Feenics.Keep.WebApi.Model.AccessLevelInfo, Feenics.Keep.WebApi.Model",
"Key": "ACCESS_LEVEL_KEY",
"CommonName": "Standard Employee Access",
"AccessLevelEntries": [
{
"$type": "Feenics.Keep.WebApi.Model.AccessLevelEntryItem, Feenics.Keep.WebApi.Model",
"ReaderId": "READER_KEY",
"ReaderCommonName": "Main Entrance Reader",
"ScheduleId": "SCHEDULE_KEY",
"ScheduleCommonName": "BusinessHours"
}
]
}'
curl -X GET \
"https://api.us.acresecurity.cloud/api/f/INSTANCE_KEY/accesslevels/ACCESS_LEVEL_KEY" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
| Practice | Recommendation |
|---|---|
| Use Keys, Not Just Names | Always populate both ReaderId and ReaderCommonName |
| Preserve Existing Entries | When adding readers, combine with existing entries to avoid overwriting |
| Consistent Schedules | Group readers by schedule for easier management |
| Document Purpose | Add notes explaining which areas each access level covers |
| Test Changes | Verify reader links work before assigning to production users |
| Audit Trail | Log access level changes for compliance |
// Get source access level
var sourceLevel = (await client.SearchAsync(currentInstance, "{\"_t\": \"AccessLevel\", \"CommonName\":\"Day Shift Access\"}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
var newSchedule = (await client.SearchAsync(currentInstance, "{\"_t\": \"Schedule\", \"CommonName\":\"NightShiftSchedule\"}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Create new access level with same readers but different schedule
var nightShiftLevel = await client.AddAccessLevelAsync(currentInstance,
new AccessLevelInfo
{
CommonName = "Night Shift Access",
AccessLevelEntries = sourceLevel.AccessLevelEntries.Select(entry =>
new AccessLevelEntryItem
{
ReaderId = entry.ReaderId,
ReaderCommonName = entry.ReaderCommonName,
ScheduleId = newSchedule.Key,
ScheduleCommonName = newSchedule.CommonName
}).ToArray()
});
| Issue | Cause | Solution |
|---|---|---|
| Access denied at reader | Reader not in access level entries | Add AccessLevelEntryItem with reader key |
| Access denied during valid hours | Wrong schedule linked | Verify ScheduleId matches intended schedule |
| Entries not saving | Missing required fields | Ensure both Id and CommonName are populated |
| Overwritten entries | Replaced array instead of appending | Combine existing entries with new ones |
| Invalid reader key | Reader moved or deleted | Refresh reader reference before linking |