Client Documentation

Kiwicodes License Manager SDK and V2 API

This guide explains how client applications integrate with the Kiwicodes License Manager using the .NET SDK or direct HMAC-SHA256 signed HTTP calls.

SDK Package KCLicenseManager.Sdk v1.1.6
API Project KCLicenseManagerAPI10
SDK Targets .NET Framework 4.8, .NET 6, .NET 8, .NET 10
Auth Model Per-request HMAC-SHA256

Overview

Kiwicodes License Manager validates software license seats using a secure, stateless request-signing model. Client applications do not request or refresh bearer tokens. Instead, every API request is signed with a public API key and a private shared secret.

Activate

Register a license seat for a specific hardware ID and product. Returns enabled features and latest app version.

Validate

Check whether a license is active, expired, disabled, blacklisted, or out of seats.

Release

Deactivate a seat or keep floating seats alive with heartbeat calls.

Manage

Create subscriptions in bulk, update existing subscriptions, or query subscription details via the API.

Consume

Track metered feature usage with consumable limits, overage control, and automatic period resets.

Base URL

The SDK default base URL is:

https://licenseviewer.kiwicodes.com/api/

If your production API is deployed under a different Azure Functions hostname, set LicenseClientOptions.BaseUrl or replace the base URL in the direct HTTP examples.

Integration Options

Option Best For Auth Handling
.NET SDK C#, WPF, WinForms, MAUI, ASP.NET, services, .NET plugins, and .NET Framework 4.8 applications. Automatic HMAC signing through HmacMessageHandler.
Direct HTTP Python, Node.js, JavaScript, shell scripts, installers, and non-.NET products. Caller computes and sends the HMAC signature on every request.

Quick Start

Install the SDK

dotnet add package KCLicenseManager.Sdk

Minimal C# Usage

using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Models;

using var client = new KiwicodesLicenseClient(new LicenseClientOptions
{
    ApiKey = "kc_pub_your_public_key",
    SharedSecret = "kc_sec_your_shared_secret",
    ProductCode = "Bonus Tools"
});

var result = await client.ActivateAsync(
    licenseKey: "ACT-KEY-123",
    hardwareId: "MACHINE-GUID-OR-STABLE-ID");

if (result.IsActive)
{
    Console.WriteLine($"License active. Seats: {result.CurrentSeats}/{result.MaxSeats}");

    // Check enabled features
    if (result.EnabledFeatures?.Contains("pro") == true)
        Console.WriteLine("Pro features unlocked.");

    // Check for updates
    if (result.LatestVersion is not null)
        Console.WriteLine($"Latest version available: {result.LatestVersion}");
}
else
{
    Console.WriteLine($"{result.Status}: {result.Description}");
}
Shared secrets are private. The shared secret is shown once when keys are generated or rotated. Store it in a secure location such as a platform secret store, encrypted configuration, or deployment secret manager.

Authentication Flow

Each request is self-authenticating. The client signs a canonical string containing a fixed signing prefix and the request date. The API validates the date, finds the subscription by API key, recomputes the signature with the stored shared secret, and compares signatures. The product code is sent alongside the signature in the Authorization header so the API can resolve the correct subscription context without requiring it in the request body.

Client App SDK or direct HTTP Build Date header Sign HMAC-SHA256 Secret never sent V2 API Authenticate Validate date + API key Compare HMAC signature Rate Limit Monthly quota check Per-tier limits Execute License & subscription operations Azure Tables KCLMSubscriptions · LMSubscriptions KCLMAllUsers · LMAPICalls · AppVersions Response License, Subscription, Features, or Error response returns to client

Required Headers

Date: Wed, 06 May 2026 12:00:00 GMT
Authorization: algorithm="hmac-sha256",headers="date",signature="BASE64_SIGNATURE",apikey="kc_pub_your_public_key",productcode="Bonus Tools"
X-Date fallback for browser environments In environments where the standard Date header is restricted and silently stripped (e.g. Blazor WebAssembly), the SDK also sends an X-Date header with the same value. The API accepts either Date or X-Date when validating clock skew. Direct HTTP callers in browser contexts should send both headers.

Signing String

kiwicodes-license
date: Wed, 06 May 2026 12:00:00 GMT

Signature Algorithm

  1. Create a UTC date string in RFC 7231 format.
  2. Build the signing string exactly as shown above, including the newline.
  3. Compute HMAC-SHA256 using the shared secret as the key.
  4. Base64 encode the binary HMAC result.
  5. Send the public API key, product code, and signature in the Authorization header.

Authorization Header Format

The Authorization header contains five comma-separated key-value pairs:

FieldRequiredDescription
algorithmYesMust be hmac-sha256.
headersYesMust be date.
signatureYesBase64-encoded HMAC-SHA256 signature.
apikeyYesYour public API key.
productcodeRecommendedProduct code/name. Sent automatically by the SDK. Used by the API to resolve subscription context for endpoints like GetSubscriptions.
Clock skew validation The API default is a 15-minute allowed clock skew, controlled by the LICENSE_AUTH__SKEW environment variable in KCLicenseManagerAPI10. Client machines should sync with NTP.

.NET SDK Reference

LicenseClientOptions

Property Required Default Description
ApiKey Yes None Public API key generated from the License Manager dashboard.
SharedSecret Yes None Private HMAC signing secret. Store securely and never send in requests.
ProductCode Recommended Empty string Default product code/name used by activate and check calls unless overridden. Also sent in the Authorization header for subscription context resolution.
BaseUrl No https://licenseviewer.kiwicodes.com/api/ API root URL. Must include the trailing /api/ path for Azure Functions deployments.
Timeout No 30 seconds HTTP request timeout.
MaxRetries No 3 Number of retries for transient 5xx responses. Client errors are not retried.

KiwicodesLicenseClient Public Methods

Method API Endpoint Purpose
ActivateAsync POST /v2/license/activate Activates or refreshes a seat for a license key and hardware ID. Returns enabled features and latest app version. Optionally accepts autodeskId and customId for additional identification.
CheckAsync GET /v2/license/check Checks whether a seat is currently active without changing seat state.
DeactivateAsync POST /v2/license/deactivate Marks a seat inactive and releases it for another machine.
HeartbeatAsync POST /v2/license/heartbeat Updates the active seat timestamp for floating license workflows.
GetSubscriptionsAsync GET /v2/subscriptions Queries subscriptions with optional filters (activation keys, email, company name, expiry date range, user data). Returns paginated results. When no filters are set, returns all subscriptions for the product. Product code is resolved from the Authorization header.
GetLMSubscriptionAsync GET /v2/public/kclm/subscription Retrieves KCLM subscription info, API call history, and a Base64-encoded usage chart. The actKey must start with "KCLM".
CreateSubscriptionsAsync POST /v2/subscriptions/create Creates one or more subscriptions in bulk.
UpdateSubscriptionAsync PUT /v2/subscriptions/update Updates an existing subscription. Only provided fields are modified.
ConsumeFeatureAsync POST /v2/consumption/consume Consumes a metered feature, incrementing the usage counter for the subscription. Returns updated count and limit info.
GetConsumptionStatusAsync POST /v2/consumption/status Queries the current consumption status for a specific feature or all consumable features on a subscription.

LicenseResponse

Property Type Description
StatusstringBusiness status such as Active, Expired, or NoSeatsAvailable.
StatusCodeintAPI business status code returned inside the response body.
Descriptionstring?Human-readable explanation.
LicenseKeystring?Activation/license key.
ProductCodestring?Product code/name associated with the request.
HardwareIdstring?The unique seat identifier sent in the request. Despite the name, this does not need to be a hardware-derived value — any string that uniquely identifies the seat is acceptable.
UserNamestring?Optional user name stored with the seat.
ComputerNamestring?Optional client computer name.
AutodeskIdstring?Autodesk account ID associated with the activation, if provided.
CustomIdstring?Custom identifier associated with the activation, if provided.
ExpiryDateDateTime?Subscription expiry date when available.
CurrentSeatsintCurrently active seat count.
MaxSeatsintMaximum seats allowed by the subscription.
IsFloatingboolWhether the subscription uses floating license behavior.
LastActivatedDateTime?Last activation or heartbeat timestamp.
CompanyNamestring?Company name from the subscription record.
Emailstring?Customer email from the subscription record.
FullNamestring?Customer full name from the subscription record.
UserData1string?Custom data field 1 from the subscription.
UserData2string?Custom data field 2 from the subscription.
EnabledFeaturesList<string>?List of feature codes enabled for this subscription (e.g. ["pro", "lte"]). Parsed from the subscription's enabledFeatures JSON field.
LatestVersionstring?Latest available application version string (e.g. "2.1.0"). Looked up from the AppVersions table by product code.
IsSuccessboolSDK convenience property. True when StatusCode == 200.
IsActiveboolSDK convenience property. True for Active or AlreadyActive.

GetSubscriptionsFilter

Optional filter object passed to GetSubscriptionsAsync. All properties are optional — when none are set, the endpoint returns all subscriptions for the authenticated product.

PropertyTypeDescription
ActivationKeysList<string>?One or more activation keys to look up (max 20). Fastest query path.
Emailstring?Exact-match filter on email address.
CompanyNamestring?Exact-match filter on company name.
ExpiryDateFromDateTime?Return subscriptions expiring on or after this date (inclusive).
ExpiryDateToDateTime?Return subscriptions expiring on or before this date (inclusive).
UserData1string?Exact-match filter on custom user data field 1.
UserData2string?Exact-match filter on custom user data field 2.
PageSizeint?Maximum results per page. Defaults to 100, server max is 1000.
ContinuationTokenstring?Opaque token from a previous response to fetch the next page.

GetSubscriptionsResponse

Paginated response returned by GetSubscriptionsAsync.

PropertyTypeDescription
SubscriptionsList<GetSubscriptionResponse>The matching subscriptions for this page.
CountintNumber of subscriptions in this page.
ContinuationTokenstring?Opaque token for fetching the next page. Null when no more results.
HasMoreboolConvenience property. True when ContinuationToken is not null.

GetSubscriptionResponse

Individual subscription record. Returned as items within GetSubscriptionsResponse.Subscriptions.

Property Type Description
ProductNamestring?Product name (partition key).
ActKeystring?Activation key (row key).
CompanyNamestring?Company name.
FullNamestring?Customer contact name.
Emailstring?Customer email.
OrderDateDateTimeOriginal order date.
SubExpiryDateDateTimeSubscription expiry date.
LastAccessedDateDateTimeDate the subscription was last accessed.
NumberOfLicensesintMaximum seat count.
DisabledboolWhether the subscription is disabled.
IsFloatingboolWhether floating licenses are enabled.
UserData1string?Custom data field 1.
UserData2string?Custom data field 2.
EnabledFeaturesList<string>?Enabled feature codes for this subscription.
UseNamedUsersboolWhether the subscription restricts activations to a pre-approved named user list.

ConsumeFeatureResponse

Returned by ConsumeFeatureAsync after consuming a metered feature.

PropertyTypeDescription
StatusstringBusiness status such as OK or LimitExceeded.
StatusCodeintAPI business status code (200 for success, 409 for limit exceeded).
Descriptionstring?Human-readable explanation.
FeatureCodestringThe feature code that was consumed.
CurrentCountintTotal consumption count after this operation.
MaxConsumptionsintMaximum allowed consumptions for this feature.
RemainingintRemaining consumptions before the limit is reached.
IsOverageboolWhether the current count exceeds the base limit (overage territory).
LastConsumedDateDateTimeTimestamp of the most recent consumption.
IsSuccessboolSDK convenience property. True when StatusCode == 200.
IsLimitExceededboolSDK convenience property. True when StatusCode == 409.

ConsumptionStatusResponse

Returned by GetConsumptionStatusAsync. Contains consumption status for one or more features.

PropertyTypeDescription
StatusstringBusiness status.
StatusCodeintAPI status code (200 for success).
LicenseKeystringThe license key queried.
FeaturesList<FeatureConsumptionStatus>Consumption status for each matching feature.
IsSuccessboolSDK convenience property. True when StatusCode == 200.

FeatureConsumptionStatus

Individual feature consumption record within a ConsumptionStatusResponse.

PropertyTypeDescription
FeatureCodestringThe feature code identifier.
FeatureNamestringHuman-readable feature name.
CurrentCountintTotal consumption count in the current period.
MaxConsumptionsintMaximum consumptions allowed per period.
RemainingintRemaining consumptions before the limit.
AllowOveragesboolWhether overages beyond the limit are permitted.
MaxOveragesintMaximum overage units allowed (when overages are enabled).
IsOverageboolWhether consumption has exceeded the base limit.
ResetPeriodstringThe reset cadence (e.g. "Monthly", "None").
LastConsumedDateDateTime?When the feature was last consumed.
LastResetDateDateTime?When the consumption counter was last reset.

KclmSubscriptionResponse

Returned by GetLMSubscriptionAsync. Contains KCLM subscription details with API usage history.

Property Type Description
SubscriptionKclmSubscriptionDtoSubscription details including company name, expiry, tier, and limits.
ApiCallHistoryList<KclmApiCallHistoryPoint>Monthly API call history for the last 6 months.
ChartImageMimeTypestringMIME type of the chart image (e.g. "image/png").
ChartImageBase64stringBase64-encoded chart image showing API call history.

Activation Key Storage

The SDK provides a static ActivationKeyManager class for saving, reading, and deleting activation keys locally. Keys are encrypted at rest using AES-256-GCM with PBKDF2-SHA512 key derivation, and stored through a pluggable storage provider system.

ActivationKeyManager Static Methods

Method Returns Purpose
SaveActivationKey void Encrypts and persists an activation key using the specified storage provider.
ReadActivationKey string? Reads and decrypts a stored activation key. Returns null if no key exists.
DeleteActivationKey bool Deletes the stored key. Returns true if the key was found and removed.
HasActivationKey bool Checks whether a stored key exists without decrypting it.

Method Parameters

Parameter Type Required Description
activationKey string Save only The plain-text activation key to encrypt and store.
passphrase string Save / Read Passphrase used to derive the AES-256 encryption key. Must be consistent across save and read calls.
productCode string All methods Product identifier used to namespace stored keys. One key is stored per product code.
storageProvider IActivationKeyStorageProvider? No Optional storage backend. Defaults to FileSystemKeyStorageProvider when omitted.

Storage Providers

The SDK ships with three built-in storage providers. All providers store data that has already been AES-256-GCM encrypted by the ActivationKeyManager.

Provider Platforms Storage Location
FileSystemKeyStorageProvider All (Windows, macOS, Linux, iOS, Android) {LocalApplicationData}/KCLicenseManager/{productCode}.key
RegistryKeyStorageProvider Windows only HKCU\SOFTWARE\KCLicenseManager\{productCode}
SecureKeyStorageProvider All (enhanced on Windows) {LocalApplicationData}/KCLicenseManager/Secure/{productCode}.skey
SecureKeyStorageProvider details On Windows, this provider wraps the already-encrypted data with an additional layer of DPAPI (ProtectedData) protection tied to the current user account. On macOS and Linux, it stores the AES-encrypted file with owner-only permissions (chmod 600). For iOS and Android native secure storage (Keychain / KeyStore), implement IActivationKeyStorageProvider using MAUI SecureStorage or platform-specific APIs.

Custom Storage Providers

Implement the IActivationKeyStorageProvider interface to create a custom storage backend. The interface requires four methods:

public interface IActivationKeyStorageProvider
{
    void   Save(string productCode, string encryptedData);
    string? Read(string productCode);
    bool   Delete(string productCode);
    bool   Exists(string productCode);
}

The encryptedData passed to Save is already AES-256-GCM encrypted and Base64-encoded. Your provider only needs to persist and retrieve this string; it does not need to handle encryption.

Encryption Details

Property Value
AlgorithmAES-256-GCM (authenticated encryption)
Key derivationPBKDF2 with SHA-512, 100,000 iterations
Nonce12 bytes, cryptographically random per encryption
Salt16 bytes, cryptographically random per encryption
Auth tag16 bytes (GCM authentication tag for tamper detection)
Stored formatBase64 of [salt 16B][nonce 12B][tag 16B][ciphertext]
Tamper detection If the stored data is modified or the wrong passphrase is provided, ReadActivationKey throws a CryptographicException. Applications should catch this and prompt the user to re-enter their activation key.

Quick Start

using KCLicenseManager.Sdk;

// Save an activation key (encrypted to local file system)
ActivationKeyManager.SaveActivationKey(
    activationKey: "ACT-KEY-123",
    passphrase:    "my-application-secret",
    productCode:   "Bonus Tools");

// Read it back
string? key = ActivationKeyManager.ReadActivationKey(
    passphrase:  "my-application-secret",
    productCode: "Bonus Tools");

// Check if one exists
bool exists = ActivationKeyManager.HasActivationKey("Bonus Tools");

// Delete it
bool deleted = ActivationKeyManager.DeleteActivationKey("Bonus Tools");

Using Different Storage Providers

using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Storage;

// Windows Registry
var registry = new RegistryKeyStorageProvider();
ActivationKeyManager.SaveActivationKey("ACT-KEY-123", "secret", "Bonus Tools", registry);

// Secure storage (DPAPI on Windows, restricted file on macOS/Linux)
var secure = new SecureKeyStorageProvider();
ActivationKeyManager.SaveActivationKey("ACT-KEY-123", "secret", "Bonus Tools", secure);

// File system with custom path
var custom = new FileSystemKeyStorageProvider(@"C:\MyApp\Licenses");
ActivationKeyManager.SaveActivationKey("ACT-KEY-123", "secret", "Bonus Tools", custom);

Typical Integration Pattern

using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Models;
using KCLicenseManager.Sdk.Storage;

// On application startup: try to load a saved activation key
var passphrase = "my-compiled-app-secret";
var productCode = "Bonus Tools";
string? savedKey = null;

try
{
    savedKey = ActivationKeyManager.ReadActivationKey(passphrase, productCode);
}
catch (System.Security.Cryptography.CryptographicException)
{
    // Stored data was tampered with or passphrase changed — prompt user to re-enter
    ActivationKeyManager.DeleteActivationKey(productCode);
}

if (savedKey is null)
{
    // Prompt user for their activation key
    savedKey = PromptUserForLicenseKey();
}

// Activate with the API
using var client = new KiwicodesLicenseClient(new LicenseClientOptions
{
    ApiKey = "kc_pub_your_public_key",
    SharedSecret = "kc_sec_your_shared_secret",
    ProductCode = productCode
});

var result = await client.ActivateAsync(savedKey, GetHardwareId());

if (result.IsActive)
{
    // Persist the key for next launch
    ActivationKeyManager.SaveActivationKey(savedKey, passphrase, productCode);

    // Use enabled features to gate functionality
    if (result.EnabledFeatures?.Contains("pro") == true)
        EnableProFeatures();
}

C# Examples

Activate, Check, Heartbeat, Deactivate

using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Exceptions;
using KCLicenseManager.Sdk.Models;

var options = new LicenseClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("KCLM_API_KEY")!,
    SharedSecret = Environment.GetEnvironmentVariable("KCLM_SHARED_SECRET")!,
    ProductCode = "Bonus Tools"
};

using var client = new KiwicodesLicenseClient(options);

var licenseKey = "ACT-KEY-123";
var hardwareId = GetHardwareId();

try
{
    var activation = await client.ActivateAsync(
        licenseKey,
        hardwareId,
        userName: Environment.UserName,
        computerName: Environment.MachineName,
        autodeskId: "AUTODESK-USER-ID",
        customId: "MY-CUSTOM-IDENTIFIER");

    if (!activation.IsActive)
    {
        Console.WriteLine($"Activation failed: {activation.Status} - {activation.Description}");
        return;
    }

    Console.WriteLine($"Active. Seats: {activation.CurrentSeats}/{activation.MaxSeats}");
    Console.WriteLine($"Company: {activation.CompanyName}");
    Console.WriteLine($"Expiry: {activation.ExpiryDate}");

    // Check enabled features
    if (activation.EnabledFeatures is { Count: > 0 })
    {
        Console.WriteLine($"Features: {string.Join(", ", activation.EnabledFeatures)}");
    }

    // Check for app updates
    if (activation.LatestVersion is not null)
    {
        Console.WriteLine($"Latest version: {activation.LatestVersion}");
    }

    var status = await client.CheckAsync(licenseKey, hardwareId);
    Console.WriteLine($"Current status: {status.Status}");

    if (status.IsFloating)
    {
        await client.HeartbeatAsync(licenseKey, hardwareId);
    }

    // Call only when the user chooses to release or transfer the license.
    // await client.DeactivateAsync(licenseKey, hardwareId);
}
catch (LicenseApiException ex) when (ex.IsUnauthorized)
{
    Console.WriteLine("License API authentication failed. Check API key and shared secret.");
}
catch (LicenseApiException ex) when (ex.IsRateLimited)
{
    Console.WriteLine("License API rate limit exceeded. Retry later.");
}

static string GetHardwareId()
{
    // Replace with your product's stable identifier strategy.
    // Despite the name, this can be any unique value — it does not
    // need to be hardware-derived (e.g. a user account ID or UUID).
    return Environment.MachineName;
}

Query Subscriptions

// Get all subscriptions for the product (no filters)
var all = await client.GetSubscriptionsAsync();
Console.WriteLine($"Total: {all.Count} subscription(s)");

// Filter by email
var byEmail = await client.GetSubscriptionsAsync(new GetSubscriptionsFilter
{
    Email = "admin@example.com"
});

foreach (var sub in byEmail.Subscriptions)
{
    Console.WriteLine($"  {sub.ActKey}: {sub.CompanyName} (expires {sub.SubExpiryDate:d})");
}

// Look up specific activation keys
var byKeys = await client.GetSubscriptionsAsync(new GetSubscriptionsFilter
{
    ActivationKeys = new List<string> { "ACT-KEY-001", "ACT-KEY-002" }
});

// Filter by expiry date range
var expiringSoon = await client.GetSubscriptionsAsync(new GetSubscriptionsFilter
{
    ExpiryDateFrom = DateTime.UtcNow,
    ExpiryDateTo = DateTime.UtcNow.AddDays(30),
    PageSize = 50
});

// Paginate through all results
var filter = new GetSubscriptionsFilter { PageSize = 100 };
do
{
    var page = await client.GetSubscriptionsAsync(filter);
    foreach (var sub in page.Subscriptions)
    {
        Console.WriteLine($"{sub.ActKey}: {sub.CompanyName}");
    }
    filter.ContinuationToken = page.ContinuationToken;
} while (filter.ContinuationToken != null);

Get KCLM Subscription Info

// Retrieve KCLM subscription details with API call history and usage chart.
// The actKey must start with "KCLM".
var kclmInfo = await client.GetLMSubscriptionAsync("KCLM-YOUR-KEY");

Console.WriteLine($"Company: {kclmInfo.Subscription.CompanyName}");
Console.WriteLine($"Tier: {kclmInfo.Subscription.Tier}");
Console.WriteLine($"Expiry: {kclmInfo.Subscription.ExpiryDate}");
Console.WriteLine($"Sub Limit: {kclmInfo.Subscription.SubscriptionLimit}");

foreach (var point in kclmInfo.ApiCallHistory)
{
    Console.WriteLine($"  {point.Month}: {point.ApiCalls} calls, {point.ActiveSubscriptions} active subs");
}

// Display or save the usage chart image
if (!string.IsNullOrEmpty(kclmInfo.ChartImageBase64))
{
    byte[] chartBytes = Convert.FromBase64String(kclmInfo.ChartImageBase64);
    File.WriteAllBytes("usage-chart.png", chartBytes);
}

Bulk Create Subscriptions

using KCLicenseManager.Sdk.Models;

var response = await client.CreateSubscriptionsAsync(new List<CreateSubscriptionItem>
{
    new()
    {
        ProductName = "Bonus Tools",
        ActKey = "ACT-KEY-001",
        CompanyName = "Example Architecture Ltd",
        Email = "admin@example.com",
        FullName = "Jane Smith",
        NumberOfLicenses = 5,
        SubExpiryDate = DateTime.UtcNow.AddYears(1),
        IsFloating = false,
        UserData1 = "Customer reference",
        UserData2 = "Sales order",
        UseNamedUsers = false
    }
});

Console.WriteLine($"{response.Count} subscriptions created.");

Update Subscription

using KCLicenseManager.Sdk.Models;

var updateResponse = await client.UpdateSubscriptionAsync(new UpdateSubscriptionItem
{
    ProductName = "Bonus Tools",
    ActKey = "ACT-KEY-001",
    CompanyName = "Updated Company Name",
    Email = "newemail@example.com",
    NumberOfLicenses = 10,
    SubExpiryDate = DateTime.UtcNow.AddYears(2),
    UseNamedUsers = true
});

Console.WriteLine(updateResponse.Message);
Partial updates Only fields you set on UpdateSubscriptionItem will be modified. Fields left as null are not changed on the server. ProductName and ActKey are always required to identify the subscription.

Consume a Feature

// Consume one unit of a metered feature
var consumeResult = await client.ConsumeFeatureAsync("ACT-KEY-123", "render-credits");

if (consumeResult.IsSuccess)
{
    Console.WriteLine($"Consumed! {consumeResult.CurrentCount}/{consumeResult.MaxConsumptions} used.");
    Console.WriteLine($"Remaining: {consumeResult.Remaining}");
}
else if (consumeResult.IsLimitExceeded)
{
    Console.WriteLine($"Limit reached. Remaining: {consumeResult.Remaining}");
}

// Consume multiple units at once
var bulkConsume = await client.ConsumeFeatureAsync("ACT-KEY-123", "render-credits", quantity: 5);

Query Consumption Status

// Get status for a specific feature
var status = await client.GetConsumptionStatusAsync("ACT-KEY-123", "render-credits");

if (status.IsSuccess)
{
    foreach (var feature in status.Features)
    {
        Console.WriteLine($"{feature.FeatureName}: {feature.CurrentCount}/{feature.MaxConsumptions}");
        Console.WriteLine($"  Remaining: {feature.Remaining}, Resets: {feature.ResetPeriod}");
    }
}

// Get status for all consumable features on the subscription
var allStatus = await client.GetConsumptionStatusAsync("ACT-KEY-123");

foreach (var feature in allStatus.Features)
{
    Console.WriteLine($"{feature.FeatureCode}: {feature.CurrentCount}/{feature.MaxConsumptions}" +
        $" (Overage: {feature.IsOverage})");
}

IHttpClientFactory Integration

using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Auth;
using KCLicenseManager.Sdk.Models;

builder.Services.AddTransient(_ => new HmacMessageHandler(apiKey, sharedSecret, productCode));

builder.Services.AddHttpClient("KiwicodesLicenseApi", client =>
{
    client.BaseAddress = new Uri("https://licenseviewer.kiwicodes.com/api/");
    client.Timeout = TimeSpan.FromSeconds(30);
})
.AddHttpMessageHandler<HmacMessageHandler>();

builder.Services.AddSingleton(sp =>
{
    var httpClient = sp.GetRequiredService<IHttpClientFactory>()
        .CreateClient("KiwicodesLicenseApi");

    return new KiwicodesLicenseClient(httpClient, new LicenseClientOptions
    {
        ApiKey = apiKey,
        SharedSecret = sharedSecret,
        ProductCode = "Bonus Tools"
    });
});

Python Examples

Reusable Signed Request Helper

import base64
import hashlib
import hmac
from datetime import datetime, timezone
from email.utils import format_datetime

import requests

API_KEY = "kc_pub_your_public_key"
SHARED_SECRET = "kc_sec_your_shared_secret"
PRODUCT_CODE = "Bonus Tools"
BASE_URL = "https://licenseviewer.kiwicodes.com/api"


def auth_headers():
    date_header = format_datetime(datetime.now(timezone.utc), usegmt=True)
    signing_string = f"kiwicodes-license\ndate: {date_header}"
    signature = base64.b64encode(
        hmac.new(
            SHARED_SECRET.encode("utf-8"),
            signing_string.encode("utf-8"),
            hashlib.sha256,
        ).digest()
    ).decode("utf-8")

    return {
        "Date": date_header,
        "Authorization": (
            f'algorithm="hmac-sha256",headers="date",'
            f'signature="{signature}",apikey="{API_KEY}",'
            f'productcode="{PRODUCT_CODE}"'
        ),
        "Content-Type": "application/json",
    }


def activate_license():
    payload = {
        "licenseKey": "ACT-KEY-123",
        "productCode": "Bonus Tools",
        "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
        "userName": "Jane Smith",
        "computerName": "WORKSTATION-01",
        "autodeskId": "AUTODESK-USER-ID",
        "customId": "MY-CUSTOM-IDENTIFIER",
    }

    response = requests.post(
        f"{BASE_URL}/v2/license/activate",
        headers=auth_headers(),
        json=payload,
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()

    if data.get("statusCode") == 200:
        print(f"Active! Seats: {data['currentSeats']}/{data['maxSeats']}")

        # Check enabled features
        features = data.get("enabledFeatures", [])
        if features:
            print(f"Features: {', '.join(features)}")

        # Check for updates
        latest = data.get("latestVersion")
        if latest:
            print(f"Latest version: {latest}")

    return data


print(activate_license())

Check and Deactivate

params = {
    "licenseKey": "ACT-KEY-123",
    "productCode": "Bonus Tools",
    "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
}

check_response = requests.get(
    f"{BASE_URL}/v2/license/check",
    headers=auth_headers(),
    params=params,
    timeout=30,
)
print(check_response.json())

deactivate_response = requests.post(
    f"{BASE_URL}/v2/license/deactivate",
    headers=auth_headers(),
    json={
        "licenseKey": "ACT-KEY-123",
        "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
    },
    timeout=30,
)
print(deactivate_response.json())

Query Subscriptions

# Get all subscriptions (no filters)
all_response = requests.get(
    f"{BASE_URL}/v2/subscriptions",
    headers=auth_headers(),
    timeout=30,
)
data = all_response.json()
print(f"Found {data['count']} subscription(s)")

# Filter by email and expiry date range
params = {
    "email": "admin@example.com",
    "expiryDateFrom": "2025-01-01",
    "expiryDateTo": "2026-12-31",
    "pageSize": 50,
}
filtered = requests.get(
    f"{BASE_URL}/v2/subscriptions",
    headers=auth_headers(),
    params=params,
    timeout=30,
)
for sub in filtered.json()["subscriptions"]:
    print(f"  {sub['actKey']}: {sub['companyName']}, Licenses: {sub['numberOfLicenses']}")

# Look up specific activation keys
keys_response = requests.get(
    f"{BASE_URL}/v2/subscriptions",
    headers=auth_headers(),
    params={"activationKeys": "ACT-KEY-001,ACT-KEY-002"},
    timeout=30,
)
print(keys_response.json())

Update Subscription

update_response = requests.put(
    f"{BASE_URL}/v2/subscriptions/update",
    headers=auth_headers(),
    json={
        "productName": "Bonus Tools",
        "actKey": "ACT-KEY-001",
        "companyName": "Updated Company Name",
        "email": "newemail@example.com",
        "numberOfLicenses": 10,
        "subExpiryDate": "2028-05-06T00:00:00Z",
        "useNamedUsers": True,
    },
    timeout=30,
)
print(update_response.json())

Consume a Feature

consume_response = requests.post(
    f"{BASE_URL}/v2/consumption/consume",
    headers=auth_headers(),
    json={
        "licenseKey": "ACT-KEY-123",
        "featureCode": "render-credits",
        "quantity": 1,
    },
    timeout=30,
)
data = consume_response.json()
if data.get("statusCode") == 200:
    print(f"Consumed! {data['currentCount']}/{data['maxConsumptions']}, remaining: {data['remaining']}")
elif data.get("statusCode") == 409:
    print(f"Limit exceeded. Remaining: {data['remaining']}")

Query Consumption Status

# Status for a specific feature
status_response = requests.post(
    f"{BASE_URL}/v2/consumption/status",
    headers=auth_headers(),
    json={"licenseKey": "ACT-KEY-123", "featureCode": "render-credits"},
    timeout=30,
)
for f in status_response.json().get("features", []):
    print(f"  {f['featureName']}: {f['currentCount']}/{f['maxConsumptions']}")

# Status for all consumable features (omit featureCode)
all_status = requests.post(
    f"{BASE_URL}/v2/consumption/status",
    headers=auth_headers(),
    json={"licenseKey": "ACT-KEY-123"},
    timeout=30,
)
for f in all_status.json().get("features", []):
    print(f"  {f['featureCode']}: {f['currentCount']}/{f['maxConsumptions']} (resets: {f['resetPeriod']})")

Node.js Examples

Reusable Signed Request Helper

import crypto from "node:crypto";

const API_KEY = "kc_pub_your_public_key";
const SHARED_SECRET = "kc_sec_your_shared_secret";
const PRODUCT_CODE = "Bonus Tools";
const BASE_URL = "https://licenseviewer.kiwicodes.com/api";

function authHeaders() {
  // RFC 7231 date format
  const dateHeader = new Date().toUTCString();

  const signingString = `kiwicodes-license\ndate: ${dateHeader}`;
  const signature = crypto
    .createHmac("sha256", SHARED_SECRET)
    .update(signingString)
    .digest("base64");

  return {
    Date: dateHeader,
    Authorization:
      `algorithm="hmac-sha256",headers="date",` +
      `signature="${signature}",apikey="${API_KEY}",` +
      `productcode="${PRODUCT_CODE}"`,
    "Content-Type": "application/json",
  };
}

Activate

async function activateLicense() {
  const response = await fetch(`${BASE_URL}/v2/license/activate`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify({
      licenseKey: "ACT-KEY-123",
      productCode: "Bonus Tools",
      hardwareId: "MACHINE-GUID-OR-STABLE-ID",
      userName: "Jane Smith",
      computerName: "WORKSTATION-01",
      autodeskId: "AUTODESK-USER-ID",
      customId: "MY-CUSTOM-IDENTIFIER",
    }),
  });

  if (!response.ok && response.status !== 409) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const data = await response.json();

  if (data.statusCode === 200) {
    console.log(`Active! Seats: ${data.currentSeats}/${data.maxSeats}`);

    // Check enabled features
    if (data.enabledFeatures?.length) {
      console.log(`Features: ${data.enabledFeatures.join(", ")}`);
    }

    // Check for updates
    if (data.latestVersion) {
      console.log(`Latest version: ${data.latestVersion}`);
    }
  } else {
    console.log(`${data.status}: ${data.description}`);
  }

  return data;
}

activateLicense().catch(console.error);

Check and Deactivate

async function checkLicense() {
  const params = new URLSearchParams({
    licenseKey: "ACT-KEY-123",
    productCode: "Bonus Tools",
    hardwareId: "MACHINE-GUID-OR-STABLE-ID",
  });

  const response = await fetch(
    `${BASE_URL}/v2/license/check?${params}`,
    { headers: authHeaders() }
  );

  return response.json();
}

async function deactivateLicense() {
  const response = await fetch(`${BASE_URL}/v2/license/deactivate`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify({
      licenseKey: "ACT-KEY-123",
      hardwareId: "MACHINE-GUID-OR-STABLE-ID",
    }),
  });

  return response.json();
}

Heartbeat

async function heartbeat() {
  const response = await fetch(`${BASE_URL}/v2/license/heartbeat`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify({
      licenseKey: "ACT-KEY-123",
      hardwareId: "MACHINE-GUID-OR-STABLE-ID",
    }),
  });

  return response.json();
}

Query Subscriptions

async function getSubscriptions(filters = {}) {
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(filters)) {
    if (value !== undefined && value !== null) {
      params.append(key, String(value));
    }
  }
  const qs = params.toString();
  const url = qs
    ? `${BASE_URL}/v2/subscriptions?${qs}`
    : `${BASE_URL}/v2/subscriptions`;

  const response = await fetch(url, { headers: authHeaders() });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const data = await response.json();
  console.log(`Found ${data.count} subscription(s)`);
  for (const sub of data.subscriptions) {
    console.log(`  ${sub.actKey}: ${sub.companyName} (${sub.numberOfLicenses} seats)`);
  }

  if (data.continuationToken) {
    console.log("More results available — pass continuationToken for next page.");
  }
  return data;
}

// Examples:
// All subscriptions
getSubscriptions();

// Filter by email
getSubscriptions({ email: "admin@example.com" });

// Filter by activation keys
getSubscriptions({ activationKeys: "ACT-KEY-001,ACT-KEY-002" });

// Filter by expiry date range with pagination
getSubscriptions({ expiryDateFrom: "2025-01-01", expiryDateTo: "2026-12-31", pageSize: 50 });

Bulk Create Subscriptions

async function createSubscriptions() {
  const response = await fetch(`${BASE_URL}/v2/subscriptions/create`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify([
      {
        productName: "Bonus Tools",
        actKey: "ACT-KEY-001",
        companyName: "Example Architecture Ltd",
        email: "admin@example.com",
        fullName: "Jane Smith",
        numberOfLicenses: 5,
        subExpiryDate: "2027-05-06T00:00:00Z",
        isFloating: false,
        userData1: "Customer reference",
        userData2: "Sales order",
        useNamedUsers: false,
      },
    ]),
  });

  const result = await response.json();
  console.log(`${result.count} subscriptions created.`);
  return result;
}

Update Subscription

async function updateSubscription() {
  const response = await fetch(`${BASE_URL}/v2/subscriptions/update`, {
    method: "PUT",
    headers: authHeaders(),
    body: JSON.stringify({
      productName: "Bonus Tools",
      actKey: "ACT-KEY-001",
      companyName: "Updated Company Name",
      email: "newemail@example.com",
      numberOfLicenses: 10,
      subExpiryDate: "2028-05-06T00:00:00Z",
      useNamedUsers: true,
    }),
  });

  return response.json();
}

Consume a Feature

async function consumeFeature() {
  const response = await fetch(`${BASE_URL}/v2/consumption/consume`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify({
      licenseKey: "ACT-KEY-123",
      featureCode: "render-credits",
      quantity: 1,
    }),
  });

  const data = await response.json();
  if (data.statusCode === 200) {
    console.log(`Consumed! ${data.currentCount}/${data.maxConsumptions}, remaining: ${data.remaining}`);
  } else if (data.statusCode === 409) {
    console.log(`Limit exceeded. Remaining: ${data.remaining}`);
  }
  return data;
}

Query Consumption Status

async function getConsumptionStatus(featureCode) {
  const body = { licenseKey: "ACT-KEY-123" };
  if (featureCode) body.featureCode = featureCode;

  const response = await fetch(`${BASE_URL}/v2/consumption/status`, {
    method: "POST",
    headers: authHeaders(),
    body: JSON.stringify(body),
  });

  const data = await response.json();
  for (const f of data.features) {
    console.log(`  ${f.featureCode}: ${f.currentCount}/${f.maxConsumptions} (resets: ${f.resetPeriod})`);
  }
  return data;
}

// Single feature
getConsumptionStatus("render-credits");

// All consumable features
getConsumptionStatus();
CommonJS alternative If your project uses CommonJS modules, replace import crypto from "node:crypto" with const crypto = require("node:crypto") and use const fetch = require("node-fetch") for Node.js versions below 18 (which include a built-in global fetch).

curl Examples

Generate Headers with OpenSSL

API_KEY="kc_pub_your_public_key"
SHARED_SECRET="kc_sec_your_shared_secret"
PRODUCT_CODE="Bonus Tools"
BASE_URL="https://licenseviewer.kiwicodes.com/api"

DATE_HEADER="$(date -u +"%a, %d %b %Y %H:%M:%S GMT")"
SIGNING_STRING="kiwicodes-license
date: ${DATE_HEADER}"
SIGNATURE="$(printf '%s' "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$SHARED_SECRET" -binary | base64)"
AUTH_HEADER="algorithm=\"hmac-sha256\",headers=\"date\",signature=\"${SIGNATURE}\",apikey=\"${API_KEY}\",productcode=\"${PRODUCT_CODE}\""

Activate

curl -X POST "${BASE_URL}/v2/license/activate" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123",
    "productCode": "Bonus Tools",
    "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
    "userName": "Jane Smith",
    "computerName": "WORKSTATION-01",
    "autodeskId": "AUTODESK-USER-ID",
    "customId": "MY-CUSTOM-IDENTIFIER"
  }'

Check

curl -X GET "${BASE_URL}/v2/license/check?licenseKey=ACT-KEY-123&productCode=Bonus%20Tools&hardwareId=MACHINE-GUID-OR-STABLE-ID" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}"

Deactivate

curl -X POST "${BASE_URL}/v2/license/deactivate" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123",
    "hardwareId": "MACHINE-GUID-OR-STABLE-ID"
  }'

Heartbeat

curl -X POST "${BASE_URL}/v2/license/heartbeat" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123",
    "hardwareId": "MACHINE-GUID-OR-STABLE-ID"
  }'

Query Subscriptions

# All subscriptions for the product
curl -X GET "${BASE_URL}/v2/subscriptions" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}"

# Filter by email
curl -X GET "${BASE_URL}/v2/subscriptions?email=admin%40example.com" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}"

# Filter by activation keys
curl -X GET "${BASE_URL}/v2/subscriptions?activationKeys=ACT-KEY-001,ACT-KEY-002" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}"

# Filter by expiry date range with pagination
curl -X GET "${BASE_URL}/v2/subscriptions?expiryDateFrom=2025-01-01&expiryDateTo=2026-12-31&pageSize=50" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}"

Update Subscription

curl -X PUT "${BASE_URL}/v2/subscriptions/update" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "productName": "Bonus Tools",
    "actKey": "ACT-KEY-001",
    "companyName": "Updated Company Name",
    "email": "newemail@example.com",
    "numberOfLicenses": 10,
    "subExpiryDate": "2028-05-06T00:00:00Z",
    "useNamedUsers": true
  }'

Consume a Feature

curl -X POST "${BASE_URL}/v2/consumption/consume" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123",
    "featureCode": "render-credits",
    "quantity": 1
  }'

Query Consumption Status

# Status for a specific feature
curl -X POST "${BASE_URL}/v2/consumption/status" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123",
    "featureCode": "render-credits"
  }'

# Status for all consumable features
curl -X POST "${BASE_URL}/v2/consumption/status" \
  -H "Content-Type: application/json" \
  -H "Date: ${DATE_HEADER}" \
  -H "Authorization: ${AUTH_HEADER}" \
  -d '{
    "licenseKey": "ACT-KEY-123"
  }'

API Reference

These endpoints are the HMAC-protected V2 endpoints used by the SDK and direct client integrations. Paths below are relative to the API root, for example https://licenseviewer.kiwicodes.com/api/v2/license/activate.

POST/v2/license/activate

Activates a license seat for a product, license key, and unique seat identifier. Returns subscription details including enabled features and latest application version.

FieldRequiredDescription
licenseKeyYesActivation/license key.
productCodeYesProduct name/code. Stored as the subscription partition key.
hardwareIdYesUnique identifier for this seat. Despite the name, this does not need to be a hardware-derived value — any string that uniquely identifies the seat is acceptable (e.g. a machine GUID, a user account ID, or an application-generated UUID).
userNameNoUser name associated with the activation.
computerNameNoComputer name associated with the activation.
autodeskIdNoAutodesk account ID associated with the activation. Useful for tracking activations by Autodesk user identity.
customIdNoCustom identifier for the activation. A free-form string for any additional identification your application requires.
{
  "licenseKey": "ACT-KEY-123",
  "productCode": "Bonus Tools",
  "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
  "userName": "Jane Smith",
  "computerName": "WORKSTATION-01",
  "autodeskId": "AUTODESK-USER-ID",
  "customId": "MY-CUSTOM-IDENTIFIER"
}

Success (HTTP 200):

{
  "status": "Active",
  "statusCode": 200,
  "description": "User added and activated successfully.",
  "licenseKey": "ACT-KEY-123",
  "productCode": "Bonus Tools",
  "hardwareId": "MACHINE-GUID-OR-STABLE-ID",
  "userName": "Jane Smith",
  "computerName": "WORKSTATION-01",
  "autodeskId": "AUTODESK-USER-ID",
  "customId": "MY-CUSTOM-IDENTIFIER",
  "expiryDate": "2027-05-06T00:00:00Z",
  "currentSeats": 1,
  "maxSeats": 5,
  "isFloating": false,
  "lastActivated": "2026-05-20T10:30:00Z",
  "companyName": "Example Architecture Ltd",
  "email": "admin@example.com",
  "fullName": "Jane Smith",
  "userData1": "Customer reference",
  "userData2": "Sales order",
  "enabledFeatures": ["pro", "lte"],
  "latestVersion": "2.1.0"
}

Business conflict: HTTP 409 with a LicenseResponse body for statuses such as NoSeatsAvailable, Expired, Disabled, Blacklisted, or NotFound.

GET/v2/license/check

Performs a read-only license status check for a specific seat.

Query ParameterRequiredDescription
licenseKeyYesActivation/license key.
productCodeYesProduct name/code.
hardwareIdYesHardware ID to validate.
/v2/license/check?licenseKey=ACT-KEY-123&productCode=Bonus%20Tools&hardwareId=MACHINE-GUID-OR-STABLE-ID

Success: HTTP 200 with LicenseResponse. The response body status can be Active, Inactive, Expired, or NotFound.

POST/v2/license/deactivate

Marks a seat as inactive so it can be used by another machine.

FieldRequiredDescription
licenseKeyYesActivation/license key.
hardwareIdYesHardware ID of the seat to release.
{
  "licenseKey": "ACT-KEY-123",
  "hardwareId": "MACHINE-GUID-OR-STABLE-ID"
}

Success: HTTP 200 with Status Deactivated.

POST/v2/license/heartbeat

Updates the seat timestamp for floating license workflows.

FieldRequiredDescription
licenseKeyYesActivation/license key.
hardwareIdYesHardware ID of the active seat.
{
  "licenseKey": "ACT-KEY-123",
  "hardwareId": "MACHINE-GUID-OR-STABLE-ID"
}

Success: HTTP 200 with Status OK.

GET/v2/subscriptions

Queries subscriptions with optional filters. All filters are passed as query parameters. When no filters are supplied, returns all subscriptions for the authenticated product (paginated). The product code is resolved from the productcode field in the Authorization header.

Query ParameterRequiredDescription
activationKeysNoComma-separated activation keys to look up (max 20). Most efficient query path.
emailNoExact-match filter on email address.
companyNameNoExact-match filter on company name.
expiryDateFromNoReturn subscriptions expiring on or after this date (ISO 8601, inclusive).
expiryDateToNoReturn subscriptions expiring on or before this date (ISO 8601, inclusive).
userData1NoExact-match filter on custom user data field 1.
userData2NoExact-match filter on custom user data field 2.
pageSizeNoMaximum results per page. Defaults to 100, max 1000.
continuationTokenNoOpaque token from a previous response to fetch the next page.
/v2/subscriptions?email=admin%40example.com&pageSize=50

Success (HTTP 200):

{
  "subscriptions": [
    {
      "productName": "Bonus Tools",
      "actKey": "ACT-KEY-123",
      "companyName": "Example Architecture Ltd",
      "fullName": "Jane Smith",
      "email": "admin@example.com",
      "orderDate": "2025-01-15T00:00:00Z",
      "subExpiryDate": "2027-05-06T00:00:00Z",
      "lastAccessedDate": "2026-05-20T10:30:00Z",
      "numberOfLicenses": 5,
      "disabled": false,
      "isFloating": false,
      "userData1": "Customer reference",
      "userData2": "Sales order",
      "enabledFeatures": ["pro", "lte"],
      "useNamedUsers": false
    }
  ],
  "count": 1,
  "continuationToken": null
}
API call surcharge When more than one subscription is returned, an additional API call cost of ceil(count × 0.5) is recorded against the SDK caller row. For example, returning 7 subscriptions adds 4 extra API calls to the monthly quota.

Empty result: HTTP 200 with an empty subscriptions array and count: 0 when no subscriptions match the filters.

GET/v2/public/kclm/subscription

Retrieves KCLM subscription info, API call history for the last 6 months, and a Base64-encoded usage chart image. This endpoint registers 2 API calls but does not count against the rate limit.

Query ParameterRequiredDescription
actKeyYesKCLM activation key. Must start with "KCLM".
/v2/public/kclm/subscription?actKey=KCLM-YOUR-KEY

Success: HTTP 200 with KclmSubscriptionResponse containing subscription details, call history array, and chart image.

POST/v2/subscriptions/create

Creates subscription records in bulk. This endpoint is HMAC-protected and exposed through CreateSubscriptionsAsync.

FieldRequiredDescription
productNameYesProduct name stored as subscription partition key.
actKeyYesActivation key stored as subscription row key.
companyNameNoCustomer company name.
emailNoCustomer email.
fullNameNoCustomer contact name.
numberOfLicensesNoSeat count. Set explicitly for useful subscriptions.
subExpiryDateNoSubscription expiry date.
isFloatingNoWhether the license is floating.
userData1NoCustom data field.
userData2NoCustom data field.
useNamedUsersNoWhether to restrict activations to a pre-approved named user list. Defaults to false.
[
  {
    "productName": "Bonus Tools",
    "actKey": "ACT-KEY-001",
    "companyName": "Example Architecture Ltd",
    "email": "admin@example.com",
    "fullName": "Jane Smith",
    "numberOfLicenses": 5,
    "subExpiryDate": "2027-05-06T00:00:00Z",
    "isFloating": false,
    "userData1": "Customer reference",
    "userData2": "Sales order",
    "useNamedUsers": false
  }
]

Success: HTTP 200 with { "message": "Added Bulk Subs", "count": 1 }.

PUT/v2/subscriptions/update

Updates an existing subscription record. Only fields included in the request body are modified; omitted fields are left unchanged. This endpoint is HMAC-protected and exposed through UpdateSubscriptionAsync.

FieldRequiredDescription
productNameYesProduct name identifying the subscription (partition key).
actKeyYesActivation key identifying the subscription (row key).
companyNameNoUpdated company name.
emailNoUpdated customer email.
fullNameNoUpdated customer contact name.
numberOfLicensesNoUpdated seat count.
subExpiryDateNoUpdated subscription expiry date.
userData1NoUpdated custom data field.
userData2NoUpdated custom data field.
useNamedUsersNoWhether to restrict activations to a pre-approved named user list. Omit to leave unchanged.
{
  "productName": "Bonus Tools",
  "actKey": "ACT-KEY-001",
  "companyName": "Updated Company Name",
  "email": "newemail@example.com",
  "numberOfLicenses": 10,
  "subExpiryDate": "2028-05-06T00:00:00Z",
  "useNamedUsers": true
}

Success: HTTP 200 with { "message": "Updated Example Corp record", "productName": "Bonus Tools", "actKey": "ACT-KEY-001" }.

Not found: HTTP 404 when no subscription exists for the given productName and actKey combination.

POST/v2/consumption/consume

Consumes a metered feature, incrementing the usage counter for the subscription. This endpoint is HMAC-protected and exposed through ConsumeFeatureAsync.

FieldRequiredDescription
licenseKeyYesActivation/license key identifying the subscription.
featureCodeYesThe feature code to consume (must be a consumable/metered feature).
quantityNoNumber of units to consume. Defaults to 1.
{
  "licenseKey": "ACT-KEY-123",
  "featureCode": "render-credits",
  "quantity": 1
}

Success (HTTP 200):

{
  "status": "OK",
  "statusCode": 200,
  "description": "Feature consumed successfully.",
  "featureCode": "render-credits",
  "currentCount": 42,
  "maxConsumptions": 100,
  "remaining": 58,
  "isOverage": false,
  "lastConsumedDate": "2026-05-26T14:30:00Z"
}

Limit exceeded: HTTP 409 with statusCode: 409 and status: "LimitExceeded" when the consumption limit has been reached and overages are not allowed.

POST/v2/consumption/status

Queries the current consumption status for a specific feature or all consumable features on a subscription. This endpoint is HMAC-protected and exposed through GetConsumptionStatusAsync.

FieldRequiredDescription
licenseKeyYesActivation/license key identifying the subscription.
featureCodeNoSpecific feature code to query. If omitted, returns all consumable features.
{
  "licenseKey": "ACT-KEY-123",
  "featureCode": "render-credits"
}

Success (HTTP 200):

{
  "status": "OK",
  "statusCode": 200,
  "licenseKey": "ACT-KEY-123",
  "features": [
    {
      "featureCode": "render-credits",
      "featureName": "Render Credits",
      "currentCount": 42,
      "maxConsumptions": 100,
      "remaining": 58,
      "allowOverages": false,
      "maxOverages": 0,
      "isOverage": false,
      "resetPeriod": "Monthly",
      "lastConsumedDate": "2026-05-26T14:30:00Z",
      "lastResetDate": "2026-05-01T00:00:00Z"
    }
  ]
}

All features: Omit featureCode from the request to receive consumption status for every consumable feature on the subscription.

Errors and Status Values

HTTP Status Codes

HTTP CodeMeaningTypical Cause
200SuccessRequest authenticated and processed.
400Bad RequestMissing body fields, query parameters, or productcode could not be resolved.
401UnauthorizedMissing date, invalid API key, signature mismatch, unsupported auth header, or clock skew failure.
404Not FoundSubscription not found for the given product name and activation key.
409ConflictActivation request authenticated, but business rules prevented activation.
429Too Many RequestsMonthly API call limit exceeded for the subscription tier.
500Server ErrorUnexpected server-side error.

LicenseResponse Status Values

StatusStatusCodeMeaning
Active200Seat activated successfully.
AlreadyActive200This hardware ID already had an active seat; timestamp was refreshed.
Inactive204Check completed; the seat is not currently active.
Blacklisted402The hardware ID is blacklisted.
Expired503The subscription expiry date has passed.
Disabled504The subscription is disabled.
NoSeatsAvailable502All allowed seats are already active.
NotFound501The subscription or license key could not be found.
Deactivated200Seat was released successfully.
OK200Heartbeat was recorded.

ErrorResponse Shape

{
  "error": "Missing Date header.",
  "code": 401,
  "details": null
}

Rate Limits

Tier NumberMonthly Calls
01,000
110,000
2100,000
31,000,000

When rate-limited, the API returns Retry-After: 3600, X-RateLimit-Limit, and X-RateLimit-Remaining: 0.

Security Notes

Protecting Secrets

  • Never send the shared secret to the API. Only the signature, public API key, and product code are sent.
  • Do not hard-code production secrets into public repositories, sample projects, client-side JavaScript, or logs.
  • Rotate keys when a shared secret may have been exposed. Rotation invalidates existing clients using the old key pair.

Hardware ID Guidance

Despite the name, the hardwareId field does not need to contain a hardware-derived value. It can be any string that uniquely identifies the seat — for example a machine GUID, a user account ID, a database row key, or an application-generated UUID. The only requirement is that the value is unique per seat and stable (i.e. it survives reboots and normal updates). Avoid IP addresses and other values that change frequently.

If you do want to use a hardware identifier, common options by platform are listed below:

PlatformCommon Option
WindowsHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
macOSIOPlatformUUID or another stable device identifier.
Linux/etc/machine-id or /var/lib/dbus/machine-id.
Node.jsUse os.hostname() combined with os.cpus() or os.networkInterfaces() MAC address for a composite ID. On Windows, child_process.execSync('wmic csproduct get UUID') returns the system UUID.

Recommended Client Behavior

  • Activate on first run or when the user enters a license key.
  • Use CheckAsync at startup or periodically for read-only validation.
  • Use HeartbeatAsync for floating licenses while the application is running.
  • Call DeactivateAsync when a user explicitly releases or transfers the seat.
  • Use GetSubscriptionsAsync to query subscription details with optional filters. Pass no filter to retrieve all subscriptions for the product.
  • Check EnabledFeatures in the activation response to gate product functionality.
  • Check LatestVersion in the activation response to prompt users about available updates.
  • Use ConsumeFeatureAsync to track metered feature usage and check IsLimitExceeded before granting access.
  • Use GetConsumptionStatusAsync to display remaining quotas to users and handle overage scenarios.
  • Handle business statuses gracefully; not every license failure is an exception.