Access levels define where and when cardholders can gain access. They’re the bridge between people and physical access points (readers). This guide shows you how to create and configure access levels.
An access level is a collection of reader-schedule pairs that define access permissions. When you assign an access level to a person, they gain access to all readers in that access level during the specified schedules.
Access Level: "Engineering Team"
├── Reader: "Lab Door" + Schedule: "Business Hours"
├── Reader: "Office Entry" + Schedule: "24/7"
└── Reader: "Parking Garage" + Schedule: "Business Hours"
| Component | Description | Required |
|---|---|---|
| CommonName | Display name for the access level | ✅ Yes |
| AccessLevelEntries | Reader + Schedule pairs | ❌ No (add later) |
| ElevatorAccessLevelEntries | Floor access for elevators | ❌ No |
| StartsOn / EndsOn | Optional date range for temporary access | ❌ No |
Start by creating an access level with just a name. You’ll add readers and schedules after they exist.
// Create a simple access level
var accessLevel = await client.AddAccessLevelAsync(currentInstance, new AccessLevelInfo
{
CommonName = "General Access"
});
Console.WriteLine($"Created access level: {accessLevel.CommonName}");
Console.WriteLine($"Key: {accessLevel.Key}");
curl -X POST \
https://api.us.acresecurity.cloud/api/f/INSTANCE_KEY/accesslevels \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"$type": "Feenics.Keep.WebApi.Model.AccessLevelInfo, Feenics.Keep.WebApi.Model",
"CommonName": "General Access"
}'
If you already have readers and schedules, you can include them at creation time.
// First, get existing readers and schedules
var reader = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"MercuryReader\\",\\"_id\\":\\"{readerKey}\\"}}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var schedule = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"Schedule\\",\\"_id\\":\\"{scheduleKey}\\"}}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Create access level with entries
var accessLevel = await client.AddAccessLevelAsync(currentInstance, new AccessLevelInfo
{
CommonName = "Engineering Team",
AccessLevelEntries = new[]
{
new AccessLevelEntryItem
{
ReaderId = reader.Key,
ReaderCommonName = reader.CommonName,
ScheduleId = schedule.Key,
ScheduleCommonName = schedule.CommonName
}
}
});
curl -X POST \
https://api.us.acresecurity.cloud/api/f/INSTANCE_KEY/accesslevels \
-H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"$type": "Feenics.Keep.WebApi.Model.AccessLevelInfo, Feenics.Keep.WebApi.Model",
"CommonName": "Engineering Team",
"AccessLevelEntries": [
{
"$type": "Feenics.Keep.WebApi.Model.AccessLevelEntryItem, Feenics.Keep.WebApi.Model",
"ReaderId": "READER_KEY",
"ReaderCommonName": "Lab Door Reader",
"ScheduleId": "SCHEDULE_KEY",
"ScheduleCommonName": "Business Hours"
}
]
}'
The most common pattern is to create readers and schedules first, then link them to access levels.
// Get the existing access level
var accessLevel = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"AccessLevel\\",\\"_id\\":\\"{accessLevelKey}\\"}}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Get the reader and schedule
var reader = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"MercuryReader\\",\\"_id\\":\\"{readerKey}\\"}}"))
.OfType<MercuryReaderInfo>().FirstOrDefault();
var schedule = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"Schedule\\",\\"_id\\":\\"{scheduleKey}\\"}}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Add new entry to the AccessLevelEntries array
var entries = accessLevel.AccessLevelEntries?.ToList() ?? new List<AccessLevelEntryItem>();
entries.Add(new AccessLevelEntryItem
{
ReaderId = reader.Key,
ReaderCommonName = reader.CommonName,
ScheduleId = schedule.Key,
ScheduleCommonName = schedule.CommonName
});
accessLevel.AccessLevelEntries = entries.ToArray();
// Update the access level with the new entries
await client.UpdateAccessLevelAsync(accessLevel);
// Get the existing access level
var accessLevel = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"AccessLevel\\",\\"_id\\":\\"{accessLevelKey}\\"}}"))
.OfType<AccessLevelInfo>().FirstOrDefault();
// Get the schedule
var schedule = (await client.SearchAsync(currentInstance, $"{{\\"_t\\":\\"Schedule\\",\\"_id\\":\\"{businessHoursKey}\\"}}"))
.OfType<ScheduleInfo>().FirstOrDefault();
// Initialize entries list from existing entries
var entries = accessLevel.AccessLevelEntries?.ToList() ?? new List<AccessLevelEntryItem>();
// Define a list of readers to add with the same schedule
var readers = new[] { "Lobby", "Elevator", "Parking" };
foreach (var readerName in readers)
{
var results = await client.SearchAsync(currentInstance,
$"{{\"_t\":\"MercuryReader\",\"CommonName\":\"{readerName}\"}}");
var reader = results.OfType<MercuryReaderInfo>().FirstOrDefault();
if (reader != null)
{
entries.Add(new AccessLevelEntryItem
{
ReaderId = reader.Key,
ReaderCommonName = reader.CommonName,
ScheduleId = schedule.Key,
ScheduleCommonName = schedule.CommonName
});
Console.WriteLine($"Added {readerName} to {accessLevel.CommonName}");
}
}
// Update the access level once with all new entries
accessLevel.AccessLevelEntries = entries.ToArray();
await client.UpdateAccessLevelAsync(accessLevel);
For contractors or visitors, create access levels with expiration dates.
var tempAccessLevel = await client.AddAccessLevelAsync(currentInstance, new AccessLevelInfo
{
CommonName = "Contractor - Project Alpha",
StartsOn = DateTime.UtcNow,
EndsOn = DateTime.UtcNow.AddMonths(3), // Expires in 3 months
AccessLevelEntries = new[]
{
new AccessLevelEntryItem
{
ReaderId = lobbyReaderKey,
ReaderCommonName = "Lobby Reader",
ScheduleId = businessHoursKey,
ScheduleCommonName = "Business Hours"
}
}
});
Console.WriteLine($"Temporary access expires: {tempAccessLevel.EndsOn}");
For elevator floor access, use ElevatorAccessLevelEntries to link readers with elevator access levels.
var elevatorAccessLevel = await client.AddAccessLevelAsync(currentInstance, new AccessLevelInfo
{
CommonName = "Executive Floors",
ElevatorAccessLevelEntries = new[]
{
new AccessLevelElevatorEntryItem
{
ReaderId = elevatorReaderKey,
ReaderCommonName = "Elevator Card Reader",
ElevatorAccessLevelId = executiveFloorsElevatorAccessLevelKey,
ElevatorAccessLevelCommonName = "Executive Floors 10-12"
}
}
});
var accessLevels = await client.SearchAsync(currentInstance, "{\"_t\":\"AccessLevel\"}");
foreach (var al in accessLevels.OfType<AccessLevelInfo>())
{
Console.WriteLine($"{al.CommonName}: {al.AccessLevelEntries?.Count ?? 0} readers");
}
var results = await client.SearchAsync(currentInstance,
"{\"_t\":\"AccessLevel\",\"CommonName\":\"Engineering Team\"}");
var accessLevel = results.OfType<AccessLevelInfo>().FirstOrDefault();
if (accessLevel != null)
{
Console.WriteLine($"Found: {accessLevel.Key}");
}
// Note: Remove all person assignments first
await client.DeleteAccessLevelAsync(accessLevel);
| Practice | Recommendation |
|---|---|
| Naming Convention | Use descriptive names: “Engineering - Lab Access” not “AL1” |
| Minimal Access | Grant only necessary access (principle of least privilege) |
| Group by Role | Create access levels that match job functions |
| Use Schedules | Must pair readers with appropriate schedules for the AccessLevel |
| Audit Regularly | Review access level assignments periodically |
| Issue | Cause | Solution |
|---|---|---|
| Access level created but no access | No reader entries added | Add entries to AccessLevelEntries array and call UpdateAccessLevelAsync - may need to Push Controller DB |
| Person has access level but denied | Schedule not active or AccessLevelEntries are empty | Check Schedule and AccessLevel configuration - may need to Push Controller DB |
| Reader not appearing in entries | Reader not linked or may no longer exist | Verify reader key and add entry |