accessgrid 1.12.0

dotnet add package accessgrid --version 1.12.0
                    
NuGet\Install-Package accessgrid -Version 1.12.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="accessgrid" Version="1.12.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="accessgrid" Version="1.12.0" />
                    
Directory.Packages.props
<PackageReference Include="accessgrid" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add accessgrid --version 1.12.0
                    
#r "nuget: accessgrid, 1.12.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package accessgrid@1.12.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=accessgrid&version=1.12.0
                    
Install as a Cake Addin
#tool nuget:?package=accessgrid&version=1.12.0
                    
Install as a Cake Tool

AccessGrid C# SDK

Official C# SDK for interacting with the AccessGrid API.

Installation

Install-Package accessgrid -Version 1.12.0

Authentication

The SDK uses a dual authentication mechanism:

  1. A static account ID sent in the X-ACCT-ID header
  2. A shared secret scheme to authenticate every API request with a signed payload in the X-PAYLOAD-SIG header

You can find both keys in your AccessGrid console on the API keys page.

Usage

Initializing the Client

using AccessGrid;
using System;

// Get credentials from environment variables
var accountId = Environment.GetEnvironmentVariable("ACCESSGRID_ACCOUNT_ID");
var secretKey = Environment.GetEnvironmentVariable("ACCESSGRID_SECRET_KEY");

// Initialize the client (default HTTP client)
using var client = new AccessGridClient(accountId, secretKey);

// Or with custom HTTP client for testing/mocking
var httpClient = new HttpClientWrapper(new HttpClient());
using var client = new AccessGridClient(accountId, secretKey, httpClient);

Listing NFC Keys

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListCardsAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    // Get filtered keys by state
    var activeKeys = await client.AccessCards.ListAsync(new ListKeysRequest
    {
        TemplateId = "05d3adb00b5",
        State = "active"
    });

    // Print keys
    foreach (var key in activeKeys)
    {
        Console.WriteLine(key);
    }
}

ListAsync returns a single page — 25 keys by default — with no indication of how many exist in total. Use ListPagedAsync when you need to know whether there are more, or to walk them all:

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListAllKeysAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var page = 1;
    while (true)
    {
        var result = await client.AccessCards.ListPagedAsync(new ListKeysRequest
        {
            TemplateId = "05d3adb00b5",
            Page = page,
            PerPage = 100
        });

        foreach (var key in result.Keys)
        {
            Console.WriteLine($"{key.Id} {key.FullName} [{key.State}]");
        }

        Console.WriteLine($"page {result.Pagination.CurrentPage} of {result.Pagination.TotalPages}, {result.Pagination.TotalCount} total");

        if (page >= result.Pagination.TotalPages) break;
        page++;
    }
}

PerPage accepts up to 100 and defaults to 25. Page defaults to 1. Both work on ListAsync too, but only ListPagedAsync exposes the Pagination block.

Issuing an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ProvisionCardAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var card = await client.AccessCards.ProvisionAsync(new ProvisionCardRequest
    {
        CardTemplateId = "05d3adb00b5",
        EmployeeId = "123456789",
        TagId = "DDEADB33FB00B5",
        FullName = "Employee name",
        Email = "employee@yourwebsite.com",
        PhoneNumber = "+19547212241",
        Classification = "full_time",
        StartDate = DateTime.UtcNow,
        ExpirationDate = DateTime.Parse("2025-02-22T21:04:03.664Z").ToUniversalTime(),
        CardNumber = "1252",
        EmployeePhoto = "[image_in_base64_encoded_format]"
    });

    Console.WriteLine($"Install URL: {card.Url}");
}

Provisioning a Multi-Family (Resident) Pass

For multi_family templates, ProvisionCardRequest carries resident-specific fields:

var card = await client.AccessCards.ProvisionAsync(new ProvisionCardRequest
{
    CardTemplateId = "0xmultifam",
    FullName = "Jane Resident",
    Email = "jane@example.com",
    StartDate = DateTime.UtcNow,
    ExpirationDate = DateTime.UtcNow.AddYears(1),
    PropertyName = "Riverside Apartments",
    PropertyAddress = "500 River Rd, Austin, TX 78701",
    BuildingName = "Building C",
    Location = "Austin",
    StorageUnit = "S-14",
    ParkingAddress = "Level 2, Spot 88",
    BarcodeData = "https://resident.example.com/jane",
    UnitNumbers = new List<string> { "C-204" },
    ParkingDetails = new List<ParkingDetail> { new ParkingDetail { Label = "Reserved", Value = "P-88" } }
});

Getting an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task GetCardAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var card = await client.AccessCards.GetAsync("0xc4rd1d");

    Console.WriteLine($"Card ID: {card.Id}");
    Console.WriteLine($"State: {card.State}");
    Console.WriteLine($"Full Name: {card.FullName}");
    Console.WriteLine($"Install URL: {card.InstallUrl}");
    Console.WriteLine($"Expiration Date: {card.ExpirationDate}");
    Console.WriteLine($"Card Number: {card.CardNumber}");
    Console.WriteLine($"Site Code: {card.SiteCode}");
    Console.WriteLine($"Devices: {card.Devices.Count}");
    Console.WriteLine($"Metadata: {card.Metadata}");
}

Updating an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task UpdateCardAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   await client.AccessCards.UpdateAsync("OtrysOXjeXSIyd1", new UpdateCardRequest
   {
       EmployeeId = "987654321",
       FullName = "Updated Employee Name",
       Classification = "contractor",
       ExpirationDate = DateTime.UtcNow.AddMonths(3),
       EmployeePhoto = "[image_in_base64_encoded_format]"
   });

   Console.WriteLine("Card updated successfully");
}

Suspending an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task SuspendCardAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   await client.AccessCards.SuspendAsync("OtrysOXjeXSIyd1");

   Console.WriteLine("Card suspended successfully");
}

Resuming an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ResumeCardAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   await client.AccessCards.ResumeAsync("OtrysOXjeXSIyd1");

   Console.WriteLine("Card resumed successfully");
}

Unlinking an NFC Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task UnlinkCardAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   await client.AccessCards.UnlinkAsync("OtrysOXjeXSIyd1");

   Console.WriteLine("Card unlinked successfully");
}

Enterprise Features

Creating a Card Template

using AccessGrid;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

public async Task CreateTemplateAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var template = await client.Console.CreateTemplateAsync(new CreateTemplateRequest
   {
       Name = "Employee Access Pass",
       Platform = "apple",
       UseCase = "corporate_id",
       Protocol = "desfire",
       AllowOnMultipleDevices = true,
       WatchCount = 2,
       IPhoneCount = 3,
       AndroidDeviceLimit = "single_device",
       BackgroundColor = "#FFFFFF",
       LabelColor = "#000000",
       LabelSecondaryColor = "#333333",
       SupportUrl = "https://help.yourcompany.com",
       SupportPhoneNumber = "+1-555-123-4567",
       SupportEmail = "support@yourcompany.com",
       PrivacyPolicyUrl = "https://yourcompany.com/privacy",
       TermsAndConditionsUrl = "https://yourcompany.com/terms",
       Background = Convert.ToBase64String(File.ReadAllBytes("background.png")),
       Logo = Convert.ToBase64String(File.ReadAllBytes("logo.png")),
       Icon = Convert.ToBase64String(File.ReadAllBytes("icon.png")),
       CredentialProfiles = new List<string> { "cp_1a2b3c" },
       LandingPages = new List<string> { "lp_4d5e6f" },
       Metadata = new Dictionary<string, object>
       {
           ["version"] = "2.1",
           ["approval_status"] = "approved"
       }
   });

   Console.WriteLine($"Template created successfully: {template.Id}");
}

Images must be PNG, 10MB or smaller, and their dimensions are validated against the template's platform and protocol. MemberPhoto takes the cardholder photo the same way.

A card template needs its images and at least one landing page before it can be published, so supplying them here saves a round trip. Apple DESFire templates also need a credential profile attached.

Updating a Card Template

using AccessGrid;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

public async Task UpdateTemplateAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var template = await client.Console.UpdateTemplateAsync(
     new UpdateTemplateRequest
     {
         CardTemplateId = "0xd3adb00b5",
         Name = "Updated Employee Access Pass",
         AllowOnMultipleDevices = true,
         WatchCount = 2,
         IPhoneCount = 3,
         AndroidDeviceLimit = "single_device",
         BackgroundColor = "#FFFFFF",
         LabelColor = "#000000",
         LabelSecondaryColor = "#333333",
         SupportUrl = "https://help.yourcompany.com",
         SupportPhoneNumber = "+1-555-123-4567",
         SupportEmail = "support@yourcompany.com",
         PrivacyPolicyUrl = "https://yourcompany.com/privacy",
         TermsAndConditionsUrl = "https://yourcompany.com/terms",
         Logo = Convert.ToBase64String(File.ReadAllBytes("new-logo.png")),
         CredentialProfiles = new List<string> { "cp_1a2b3c" },
         LandingPages = new List<string> { "lp_4d5e6f" },
         Metadata = new Dictionary<string, object>
         {
             ["department"] = "engineering",
             ["cost_center"] = "1234"
         }
     }
   );

   Console.WriteLine($"Template updated successfully: {template.Id}");
   Console.WriteLine($"Department: {template.Metadata["department"]}");
}

Each image you send replaces the one on the template, and images you leave out are untouched. CredentialProfiles and LandingPages replace the whole set: leave them unset to keep the current attachments, or pass an empty list to clear them.

Listing Card Templates

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListTemplatesAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var result = await client.Console.ListTemplatesAsync(page: 1, perPage: 50);

   foreach (var template in result.Templates)
   {
       Console.WriteLine($"{template.Id} [{template.Status}] {template.Name}");
   }

   Console.WriteLine($"Page {result.Pagination.CurrentPage} of {result.Pagination.TotalPages}");
}

Templates come back newest first, 50 per page by default and 100 at most. Both arguments are optional.

Each entry is a summary. The key counts, image URLs and attached credential profiles and landing pages are only returned by ReadTemplateAsync. Card template pairs aren't included here; ListPassTemplatePairsAsync lists those.

Reading a Card Template

using AccessGrid;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public async Task ReadTemplateAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var template = await client.Console.ReadTemplateAsync("0xd3adb00b5");

   Console.WriteLine($"Template ID: {template.Id}");
   Console.WriteLine($"Name: {template.Name}");
   Console.WriteLine($"Platform: {template.Platform}");
   Console.WriteLine($"Protocol: {template.Protocol}");
   Console.WriteLine($"Device counts: {template.AllowedDeviceCounts}");

   foreach (var entry in template.Metadata ?? new Dictionary<string, object>())
   {
       Console.WriteLine($"{entry.Key}: {entry.Value}");
   }
}

Publishing a Card Template

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task PublishTemplateAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var result = await client.Console.PublishTemplateAsync("0xd3adb00b5");

    Console.WriteLine($"Template ID: {result.Id}");
    Console.WriteLine($"Status: {result.Status}");
}

Deleting a Card Template

await client.Console.DeleteTemplateAsync("0xd3adb00b5");

Revealing a SmartTap Private Key

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task RevealTemplatePrivateKeyAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   // SDK generates a P-256 keypair locally, submits the public key, and
   // decrypts the server's response. The private key never leaves the host.
   var result = await client.Console.RevealTemplatePrivateKeyAsync("0xd3adb00b5");

   Console.WriteLine($"Key version: {result.KeyVersion}");
   Console.WriteLine($"Collector ID: {result.CollectorId}");
   Console.WriteLine($"Fingerprint: {result.Fingerprint}");
   Console.WriteLine(result.PrivateKey); // PEM — store in your reader/collector key vault
}

Reading Event Logs

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task GetEventLogAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var events = await client.Console.EventLogAsync(
       "0xd3adb00b5",
       new EventLogFilters
       {
           Device = "mobile",
           StartDate = DateTime.UtcNow.AddDays(-30),
           EndDate = DateTime.UtcNow,
           EventType = "install"
       });

   foreach (var evt in events)
   {
       Console.WriteLine($"Event: {evt.Type} at {evt.Timestamp} by {evt.UserId}");
   }
}

Listing Pass Template Pairs

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListPassTemplatePairsAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   // List first page with default page size (50)
   var result = await client.Console.ListPassTemplatePairsAsync();

   foreach (var pair in result.PassTemplatePairs)
   {
       Console.WriteLine($"Pair: {pair.Name} ({pair.Id})");
       Console.WriteLine($"  iOS: {pair.IosTemplate?.Name ?? "none"}");
       Console.WriteLine($"  Android: {pair.AndroidTemplate?.Name ?? "none"}");
   }

   Console.WriteLine($"Page {result.Pagination.CurrentPage} of {result.Pagination.TotalPages}");

   // Or with pagination
   var page2 = await client.Console.ListPassTemplatePairsAsync(page: 2, perPage: 10);
}

Creating a Pass Template Pair

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CreatePassTemplatePairAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   // Both templates must be published (status: ready) and use the same protocol.
   var pair = await client.Console.CreatePassTemplatePairAsync(new CreatePassTemplatePairRequest
   {
       Name = "Employee Badge Pair",
       AppleCardTemplateId = "0xapplet3mp14t3",
       GoogleCardTemplateId = "0xgoogl3t3mp14t3"
   });

   Console.WriteLine($"Created pair: {pair.Name} ({pair.Id})");
}

Renaming a Pass Template Pair

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task UpdatePassTemplatePairAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   var pair = await client.Console.UpdatePassTemplatePairAsync("0xpa1rt3mp14t3", new UpdatePassTemplatePairRequest
   {
       Name = "Contractor Badge Pair"
   });

   Console.WriteLine($"Pair renamed: {pair.Name}");
}

Only the name can be changed. The two card templates a pair links are fixed once it exists.

Deleting a Pass Template Pair

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task DeletePassTemplatePairAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   await client.Console.DeletePassTemplatePairAsync("0xpa1rt3mp14t3");

   Console.WriteLine("Pair deleted");
}

A pair can only be deleted once every key issued through it has been deleted. If any are still live, the call throws and the error names how many. Deleting the pair leaves both of its card templates in place, so delete those separately with DeleteTemplateAsync if you no longer need them.

Getting Ledger Items

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task GetLedgerItemsAsync()
{
   var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
   var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

   using var client = new AccessGridClient(accountId, secretKey);

   // Get all ledger items
   var result = await client.Console.GetLedgerItemsAsync();

   foreach (var item in result.LedgerItems)
   {
       Console.WriteLine($"{item.CreatedAt} | {item.Kind} | {item.Amount} | {item.ExId}");

       if (item.AccessPass != null)
       {
           Console.WriteLine($"  Pass: {item.AccessPass.FullName} ({item.AccessPass.State})");

           if (item.AccessPass.PassTemplate != null)
               Console.WriteLine($"  Template: {item.AccessPass.PassTemplate.Name}");
       }
   }

   Console.WriteLine($"Page {result.Pagination.CurrentPage} of {result.Pagination.TotalPages}");

   // With date filters and pagination
   var filtered = await client.Console.GetLedgerItemsAsync(
       startDate: DateTime.UtcNow.AddDays(-30),
       endDate: DateTime.UtcNow,
       page: 1,
       perPage: 25
   );
}

iOS In-App Provisioning Preflight

using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task GenerateProvisioningCredentialsAsync()
{
    var client = new AccessGridClient(
        Environment.GetEnvironmentVariable("ACCOUNT_ID"),
        Environment.GetEnvironmentVariable("SECRET_KEY")
    );

    var response = await client.Console.IosPreflightAsync(
        cardTemplateId: "0xt3mp14t3-3x1d",
        accessPassExId: "0xp455-3x1d"
    );

    Console.WriteLine($"Provisioning Credential ID: {response.ProvisioningCredentialIdentifier}");
    Console.WriteLine($"Sharing Instance ID: {response.SharingInstanceIdentifier}");
    Console.WriteLine($"Card Template ID: {response.CardTemplateIdentifier}");
    Console.WriteLine($"Environment ID: {response.EnvironmentIdentifier}");
}

Landing Pages

List Landing Pages
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListLandingPagesAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var landingPages = await client.Console.ListLandingPagesAsync();

    foreach (var page in landingPages)
    {
        Console.WriteLine($"ID: {page.Id}, Name: {page.Name}, Kind: {page.Kind}");
        Console.WriteLine($"  Password Protected: {page.PasswordProtected}");
        if (page.LogoUrl != null)
            Console.WriteLine($"  Logo URL: {page.LogoUrl}");
    }
}
Create a Landing Page
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CreateLandingPageAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var landingPage = await client.Console.CreateLandingPageAsync(new CreateLandingPageRequest
    {
        Name = "Miami Office Access Pass",
        Kind = "universal", // "universal" or "personalized"
        AdditionalText = "Welcome to the Miami Office",
        BgColor = "#f1f5f9",
        AllowImmediateDownload = true
    });

    Console.WriteLine($"Landing page created: {landingPage.Id}");
    Console.WriteLine($"Name: {landingPage.Name}, Kind: {landingPage.Kind}");
}
Update a Landing Page
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task UpdateLandingPageAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var landingPage = await client.Console.UpdateLandingPageAsync("0xlandingpage1d", new UpdateLandingPageRequest
    {
        Name = "Updated Miami Office Access Pass",
        AdditionalText = "Welcome! Tap below to get your access pass.",
        BgColor = "#e2e8f0"
    });

    Console.WriteLine($"Landing page updated: {landingPage.Id}");
    Console.WriteLine($"Name: {landingPage.Name}");
}
Delete a Landing Page
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task DeleteLandingPageAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    await client.Console.DeleteLandingPageAsync("0xlandingpage1d");

    Console.WriteLine("Landing page deleted");
}

A landing page can only be deleted once no active card template is attached to it. If any still are, the call throws and the error names how many, so detach them first. Deleting a landing page twice throws the second time, because a deleted page is no longer found.

Credential Profiles

List Credential Profiles
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListProfilesAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var profiles = await client.Console.CredentialProfiles.ListAsync();

    foreach (var profile in profiles)
    {
        Console.WriteLine($"ID: {profile.Id}, Name: {profile.Name}, AID: {profile.Aid}");
    }
}
Create a Credential Profile
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CreateProfileAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    var profile = await client.Console.CredentialProfiles.CreateAsync(new CreateCredentialProfileRequest
    {
        Name = "Main Office Profile",
        AppName = "KEY-ID-main",
        Keys = new[]
        {
            new KeyParam { Value = "your_32_char_hex_master_key_here" },
            new KeyParam { Value = "your_32_char_hex__read_key__here" }
        }
    });

    Console.WriteLine($"Profile created: {profile.Id}");
    Console.WriteLine($"AID: {profile.Aid}");
}
Rename a Credential Profile
var profile = await client.Console.CredentialProfiles.UpdateAsync("a1b2c3d4e5f", new UpdateCredentialProfileRequest
{
    Name = "Miami Office Profile"
});

Console.WriteLine($"Profile renamed: {profile.Name}");

Only the name can be changed. Keys, application and file settings are fixed once the profile exists.

Delete a Credential Profile
await client.Console.CredentialProfiles.DeleteAsync("a1b2c3d4e5f");

Webhooks

using AccessGrid;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public async Task ManageWebhooksAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    using var client = new AccessGridClient(accountId, secretKey);

    // List webhooks
    var webhooks = await client.Console.Webhooks.ListAsync();
    foreach (var wh in webhooks.Webhooks)
    {
        Console.WriteLine($"Webhook: {wh.Name} ({wh.Id})");
    }

    // Create a webhook
    var newWebhook = await client.Console.Webhooks.CreateAsync(new CreateWebhookRequest
    {
        Name = "My Webhook",
        Url = "https://example.com/webhook",
        SubscribedEvents = new List<string> { "ag.access_pass.issued", "ag.access_pass.activated" }
    });
    Console.WriteLine($"Created webhook: {newWebhook.Id}");
    Console.WriteLine($"Private key: {newWebhook.PrivateKey}");

    // Verify a webhook
    var verification = await client.Console.Webhooks.VerifyAsync(newWebhook.Id);
    Console.WriteLine($"Verified: {verification.Verified}");

    // Delete a webhook
    await client.Console.Webhooks.DeleteAsync(newWebhook.Id);
}
Handling incoming webhook events

Deliveries arrive as CloudEvents JSON with Content-Type: application/cloudevents+json. The SDK provides types for the data block of each event; you parse the envelope and dispatch on its type field.

Envelope field names are lowercase (specversion, dataschema), so deserialize with PropertyNameCaseInsensitive = true or your envelope properties will silently come back null.

using AccessGrid;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;

public record CloudEvent(string SpecVersion, string Id, string Source, string Type, string Time, JsonElement Data);

public class WebhookHandler
{
    private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true,
        Converters = { new JsonStringEnumConverter() }
    };

    public void HandleDelivery(string body)
    {
        var envelope = JsonSerializer.Deserialize<CloudEvent>(body, Options);
        if (envelope is null) return;

        var data = envelope.Data.GetRawText();

        switch (envelope.Type)
        {
            case "ag.access_pass.issued":
                var issued = JsonSerializer.Deserialize<AccessPassEvent>(data, Options);
                // Issued events carry Details; every other pass event carries Devices instead
                if (issued?.Details is not null)
                {
                    Console.WriteLine($"{issued.Id} issued on {issued.Details.Platform}");
                }
                break;

            case "ag.access_pass.activated":
                var activated = JsonSerializer.Deserialize<AccessPassEvent>(data, Options);
                if (activated?.Devices is not null)
                {
                    foreach (var device in activated.Devices)
                    {
                        Console.WriteLine($"{activated.Id} active on {device.Type}");
                    }
                }
                break;

            case "ag.landing_page.created":
                var page = JsonSerializer.Deserialize<LandingPageEvent>(data, Options);
                Console.WriteLine($"Landing page {page?.Id} created");
                break;
        }
    }
}

Each event family has an enum of its event names and a type for its payload. The enums deserialize from the envelope's type string if you prefer switching on a typed value over a literal:

var eventType = JsonSerializer.Deserialize<AccessPassEventType>($"\"{envelope.Type}\"", Options);
Event family Event names Payload type
Access pass AccessPassEventType AccessPassEvent
Card template CardTemplateEventType CardTemplateEvent
Card template pair CardTemplatePairEventType CardTemplatePairEvent
Landing page LandingPageEventType LandingPageEvent
Credential profile CredentialProfileEventType CredentialProfileEvent
HID organization HIDOrgEventType HIDOrgEvent
Account balance AccountBalanceEventType AccountBalanceEvent
Webhook WebhookEventType WebhookEvent

HID Organizations

Create an HID org
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CreateOrgAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    var client = new AccessGridClient(accountId, secretKey);

    var org = await client.Console.HID.Orgs.CreateAsync(new CreateHIDOrgRequest
    {
        Name = "My Org",
        Email = "admin@example.com",
        FullAddress = "1 Main St, NY NY",
        Phone = "+1-555-0000",
        FirstName = "Ada",
        LastName = "Lovelace"
    });

    Console.WriteLine($"Created org: {org.Name} (ID: {org.Id})");
    Console.WriteLine($"Slug: {org.Slug}");
}
List HID orgs
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task ListOrgsAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    var client = new AccessGridClient(accountId, secretKey);

    var orgs = await client.Console.HID.Orgs.ListAsync();

    foreach (var org in orgs)
    {
        Console.WriteLine($"Org ID: {org.Id}, Name: {org.Name}, Slug: {org.Slug}");
    }
}
Activate an HID org
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CompleteRegistrationAsync()
{
    var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID");
    var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY");

    var client = new AccessGridClient(accountId, secretKey);

    var result = await client.Console.HID.Orgs.ActivateAsync(new CompleteHIDOrgRequest
    {
        Email = "admin@example.com",
        Password = "hid-password-123"
    });

    Console.WriteLine($"Completed registration for org: {result.Name}");
    Console.WriteLine($"Status: {result.Status}");
}

Testing Your Application Code

When building applications that use the AccessGrid SDK, you'll want to test your own business logic without making actual API calls. Here are examples of how to test your application code that calls the AccessGrid library.

Testing Dependencies

For testing, you'll need to install a mocking framework. We recommend Moq with either xUnit or NUnit:

Install-Package Moq
Install-Package Microsoft.NET.Test.Sdk

# For xUnit
Install-Package xunit
Install-Package xunit.runner.visualstudio

# For NUnit
Install-Package NUnit
Install-Package NUnit3TestAdapter

Example 1: Testing Employee Onboarding Service (XUnit)

Let's say you have an employee onboarding service in your application:

// Your application service that uses AccessGrid
public class EmployeeOnboardingService
{
    private readonly IAccessGridClient _accessGridClient;
    private readonly ILogger<EmployeeOnboardingService> _logger;

    public EmployeeOnboardingService(IAccessGridClient accessGridClient, ILogger<EmployeeOnboardingService> logger)
    {
        _accessGridClient = accessGridClient;
        _logger = logger;
    }

    public async Task<OnboardingResult> OnboardEmployeeAsync(Employee employee)
    {
        try
        {
            // Your business logic here
            var provisionRequest = new ProvisionCardRequest
            {
                CardTemplateId = "your-template-id",
                EmployeeId = employee.Id,
                FullName = employee.FullName,
                Email = employee.Email,
                Classification = employee.Department == "Security" ? "security_personnel" : "employee",
                StartDate = employee.StartDate,
                ExpirationDate = employee.StartDate.AddYears(1)
            };

            var card = await _accessGridClient.AccessCards.ProvisionAsync(provisionRequest);

            _logger.LogInformation("Access card provisioned for employee {employee.Id}: {card.Id}");
            
            return new OnboardingResult
            {
                Success = true,
                CardId = card.Id,
                InstallUrl = card.Url,
                EmployeeId = employee.Id
            };
        }
        catch (AccessGridException ex)
        {
            _logger.LogError($"Failed to provision access card for employee {employee.Id}", ex);
            return new OnboardingResult { Success = false, ErrorMessage = ex.Message };
        }
    }
}

// Your test class
public class EmployeeOnboardingServiceTests
{
    [Fact]
    public async Task OnboardEmployeeAsync_ShouldReturnSuccess_WhenProvisioningSucceeds()
    {
        // Arrange
        var mockClient = new Mock<IAccessGridClient>();
        var mockApiService = new Mock<IApiService>();
        var mockLogger = new Mock<ILogger<EmployeeOnboardingService>>();

        var employee = new Employee
        {
            Id = "EMP123",
            FullName = "John Smith",
            Email = "john.smith@company.com",
            Department = "Engineering",
            StartDate = DateTime.UtcNow
        };

        var expectedCard = new AccessCard
        {
            Id = "card-123",
            FullName = "John Smith",
            State = "active",
            Url = "https://install.accessgrid.com/card-123"
        };

        mockApiService
            .Setup(x => x.PostAsync<AccessCard>("/v1/key-cards", It.IsAny<ProvisionCardRequest>()))
            .ReturnsAsync(expectedCard);

        var accessCardsService = new AccessCardsService(mockApiService.Object);
        mockClient.SetupGet(x => x.AccessCards).Returns(accessCardsService);

        var service = new EmployeeOnboardingService(mockClient.Object, mockLogger.Object);

        // Act
        var result = await service.OnboardEmployeeAsync(employee);

        // Assert
        Assert.True(result.Success);
        Assert.Equal("card-123", result.CardId);
        Assert.Equal("https://install.accessgrid.com/card-123", result.InstallUrl);
        Assert.Equal("EMP123", result.EmployeeId);

        // Verify the correct request was made
        mockApiService.Verify(x => x.PostAsync<AccessCard>("/v1/key-cards", It.Is<ProvisionCardRequest>(req =>
            req.EmployeeId == "EMP123" &&
            req.FullName == "John Smith" &&
            req.Classification == "employee")), Times.Once);
    }

    [Fact]
    public async Task OnboardEmployeeAsync_ShouldReturnFailure_WhenAccessGridThrowsException()
    {
        // Arrange
        var mockClient = new Mock<IAccessGridClient>();
        var mockApiService = new Mock<IApiService>();
        var mockLogger = new Mock<ILogger<EmployeeOnboardingService>>();

        var employee = new Employee { Id = "EMP123", FullName = "John Smith", Email = "john@company.com" };

        mockApiService
            .Setup(x => x.PostAsync<AccessCard>("/v1/key-cards", It.IsAny<ProvisionCardRequest>()))
            .ThrowsAsync(new AccessGridException("API rate limit exceeded"));

        var accessCardsService = new AccessCardsService(mockApiService.Object);
        mockClient.SetupGet(x => x.AccessCards).Returns(accessCardsService);

        var service = new EmployeeOnboardingService(mockClient.Object, mockLogger.Object);

        // Act
        var result = await service.OnboardEmployeeAsync(employee);

        // Assert
        Assert.False(result.Success);
        Assert.Equal("API rate limit exceeded", result.ErrorMessage);
        Assert.Null(result.CardId);
    }
}

Example 2: Testing Minimal API Web Application (NUnit)

Here's how you might test a modern .NET 8 minimal API application using NUnit:

// Your minimal API application (Program.cs)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register AccessGrid client
builder.Services.AddSingleton<IAccessGridClient>(provider =>
{
    var accountId = builder.Configuration["AccessGrid:AccountId"];
    var secretKey = builder.Configuration["AccessGrid:SecretKey"];
    return new AccessGridClient(accountId, secretKey);
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Minimal API endpoints
app.MapGet("/api/accesscards", async (string templateId, IAccessGridClient accessGridClient) =>
{
    var cards = await accessGridClient.AccessCards.ListAsync(new ListKeysRequest
    {
        TemplateId = templateId,
        State = "active"
    });

    return Results.Ok(cards);
})
.WithName("GetAccessCards")
.WithOpenApi();

app.MapPost("/api/accesscards/{cardId}/suspend", async (string cardId, IAccessGridClient accessGridClient) =>
{
    var suspendedCard = await accessGridClient.AccessCards.SuspendAsync(cardId);
    return Results.Ok(suspendedCard);
})
.WithName("SuspendCard")
.WithOpenApi();

app.Run();

// Your test class using NUnit
[TestFixture]
public class AccessCardsApiTests
{
    private WebApplication _app = null!;
    private HttpClient _client = null!;
    private Mock<IAccessGridClient> _mockAccessGridClient = null!;
    private Mock<IApiService> _mockApiService = null!;

    [SetUp]
    public void Setup()
    {
        _mockAccessGridClient = new Mock<IAccessGridClient>();
        _mockApiService = new Mock<IApiService>();

        var builder = WebApplication.CreateBuilder();
        builder.Services.AddEndpointsApiExplorer();

        // Override the AccessGrid client with our mock
        builder.Services.AddSingleton(_mockAccessGridClient.Object);

        _app = builder.Build();

        // Configure the same endpoints as your main application
        _app.MapGet("/api/accesscards", async (string templateId, IAccessGridClient accessGridClient) =>
        {
            var cards = await accessGridClient.AccessCards.ListAsync(new ListKeysRequest
            {
                TemplateId = templateId,
                State = "active"
            });
            return Results.Ok(cards);
        });

        _app.MapPost("/api/accesscards/{cardId}/suspend", async (string cardId, IAccessGridClient accessGridClient) =>
        {
            var suspendedCard = await accessGridClient.AccessCards.SuspendAsync(cardId);
            return Results.Ok(suspendedCard);
        });

        _client = new HttpClient();
    }

    [TearDown]
    public void TearDown()
    {
        _client?.Dispose();
        _app?.DisposeAsync();
    }

    [Test]
    public async Task GetAccessCards_ShouldReturnOkWithCards_WhenCardsExist()
    {
        // Arrange
        var expectedCards = new List<AccessCard>
        {
            new() { Id = "card1", FullName = "John Smith", State = "active" },
            new() { Id = "card2", FullName = "Jane Doe", State = "active" }
        };

        var keysListResponse = new KeysListResponse { Keys = expectedCards };
        _mockApiService
            .Setup(x => x.GetAsync<KeysListResponse>("/v1/key-cards", It.IsAny<Dictionary<string, string>>()))
            .ReturnsAsync(keysListResponse);

        var accessCardsService = new AccessCardsService(_mockApiService.Object);
        _mockAccessGridClient.SetupGet(x => x.AccessCards).Returns(accessCardsService);

        // Act
        await _app.StartAsync();
        var response = await _client.GetAsync($"{_app.Urls.First()}/api/accesscards?templateId=test-template");

        // Assert
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));

        var content = await response.Content.ReadAsStringAsync();
        var returnedCards = JsonSerializer.Deserialize<List<AccessCard>>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        Assert.That(returnedCards, Is.Not.Null);
        Assert.That(returnedCards.Count, Is.EqualTo(2));
        Assert.That(returnedCards[0].FullName, Is.EqualTo("John Smith"));

        _mockApiService.Verify(x => x.GetAsync<KeysListResponse>("/v1/key-cards", It.IsAny<Dictionary<string, string>>()), Times.Once);
    }

    [Test]
    public async Task SuspendCard_ShouldReturnOkWithSuspendedCard_WhenSuspensionSucceeds()
    {
        // Arrange
        var expectedCard = new AccessCard { Id = "card123", State = "suspended" };

        _mockApiService
            .Setup(x => x.PostAsync<AccessCard>("/v1/key-cards/card123/suspend", null))
            .ReturnsAsync(expectedCard);

        var accessCardsService = new AccessCardsService(_mockApiService.Object);
        _mockAccessGridClient.SetupGet(x => x.AccessCards).Returns(accessCardsService);

        // Act
        await _app.StartAsync();
        var response = await _client.PostAsync($"{_app.Urls.First()}/api/accesscards/card123/suspend", null);

        // Assert
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));

        var content = await response.Content.ReadAsStringAsync();
        var result = JsonSerializer.Deserialize<AccessCard>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

        Assert.That(result, Is.Not.Null);
        Assert.That(result.Id, Is.EqualTo("card123"));
        Assert.That(result.State, Is.EqualTo("suspended"));

        _mockApiService.Verify(x => x.PostAsync<AccessCard>("/v1/key-cards/card123/suspend", null), Times.Once);
    }
}

Feature Matrix

Endpoint Method Supported
POST /v1/key-cards AccessCards.ProvisionAsync() Y
GET /v1/key-cards/{id} AccessCards.GetAsync() Y
PATCH /v1/key-cards/{id} AccessCards.UpdateAsync() Y
GET /v1/key-cards AccessCards.ListAsync(), AccessCards.ListPagedAsync() Y
POST /v1/key-cards/{id}/suspend AccessCards.SuspendAsync() Y
POST /v1/key-cards/{id}/resume AccessCards.ResumeAsync() Y
POST /v1/key-cards/{id}/unlink AccessCards.UnlinkAsync() Y
POST /v1/key-cards/{id}/delete AccessCards.DeleteAsync() Y
POST /v1/console/card-templates Console.CreateTemplateAsync() Y
PUT /v1/console/card-templates/{id} Console.UpdateTemplateAsync() Y
GET /v1/console/card-templates Console.ListTemplatesAsync() Y
GET /v1/console/card-templates/{id} Console.ReadTemplateAsync() Y
POST /v1/console/card-templates/{id}/publish Console.PublishTemplateAsync() Y
POST /v1/console/card-templates/{id}/smart-tap/reveal Console.RevealTemplatePrivateKeyAsync() Y
DELETE /v1/console/card-templates/{id} Console.DeleteTemplateAsync() Y
GET /v1/console/card-templates/{id}/logs Console.EventLogAsync() Y
GET /v1/console/card-template-pairs Console.ListPassTemplatePairsAsync() Y
POST /v1/console/card-template-pairs Console.CreatePassTemplatePairAsync() Y
PUT /v1/console/card-template-pairs/{id} Console.UpdatePassTemplatePairAsync() Y
DELETE /v1/console/card-template-pairs/{id} Console.DeletePassTemplatePairAsync() Y
POST /v1/console/card-templates/{id}/ios_preflight Console.IosPreflightAsync() Y
GET /v1/console/ledger-items Console.GetLedgerItemsAsync() Y
GET /v1/console/landing-pages Console.ListLandingPagesAsync() Y
POST /v1/console/landing-pages Console.CreateLandingPageAsync() Y
PUT /v1/console/landing-pages/{id} Console.UpdateLandingPageAsync() Y
DELETE /v1/console/landing-pages/{id} Console.DeleteLandingPageAsync() Y
GET /v1/console/credential-profiles Console.CredentialProfiles.ListAsync() Y
POST /v1/console/credential-profiles Console.CredentialProfiles.CreateAsync() Y
PUT /v1/console/credential-profiles/{id} Console.CredentialProfiles.UpdateAsync() Y
DELETE /v1/console/credential-profiles/{id} Console.CredentialProfiles.DeleteAsync() Y
GET /v1/console/webhooks Console.Webhooks.ListAsync() Y
POST /v1/console/webhooks Console.Webhooks.CreateAsync() Y
DELETE /v1/console/webhooks/{id} Console.Webhooks.DeleteAsync() Y
POST /v1/console/webhooks/{id}/verify Console.Webhooks.VerifyAsync() Y
POST /v1/console/hid/orgs Console.HID.Orgs.CreateAsync() Y
POST /v1/console/hid/orgs/activate Console.HID.Orgs.ActivateAsync() Y
GET /v1/console/hid/orgs Console.HID.Orgs.ListAsync() Y

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.12.0 42 9/11/2026
1.11.0 49 9/11/2026
1.10.0 42 9/11/2026
1.9.0 49 9/11/2026
1.8.1 142 8/19/2026
1.8.0 110 8/14/2026
1.7.0 126 8/4/2026
1.6.0 134 6/30/2026
1.5.1 117 6/4/2026
1.5.0 5,061 4/16/2026
1.2.2 1,074 3/18/2026
1.2.1 131 3/12/2026
1.2.0 118 3/12/2026
1.1.8 925 2/10/2026
1.1.7 133 2/10/2026
1.1.6 296 1/9/2026
1.1.5 133 1/8/2026
1.1.4 132 12/30/2025
1.1.3 527 11/25/2025
1.1.2 228 11/25/2025
Loading failed