Link a Reader to an Access Level

Overview

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:

  • Reader - The physical device at a door
  • Schedule - When access is allowed
  • Access Level - The permission set assigned to people

AccessLevelEntryItem Properties

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

Prerequisites

Before linking readers to access levels, ensure you have:

  1. Access Level - Created via Create Access Level
  2. Reader(s) - Added to a controller via Add Reader
  3. Schedule(s) - Created via Create Schedule

Linking a Single Reader to an Access Level

// 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}");

Linking Multiple Readers to an Access Level

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);

Adding Readers to Existing Entries

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);

Removing a Reader from an Access Level

// 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 Examples

Update Access Level with Reader Entry

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"

Best Practices

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

Common Patterns

Clone Access Level with Modified Schedule

// 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()
    });

Troubleshooting

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