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.
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}");
}
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.
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"
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
- Create a UTC date string in RFC 7231 format.
- Build the signing string exactly as shown above, including the newline.
- Compute HMAC-SHA256 using the shared secret as the key.
- Base64 encode the binary HMAC result.
- Send the public API key, product code, and signature in the
Authorizationheader.
Authorization Header Format
The Authorization header contains five comma-separated key-value pairs:
| Field | Required | Description |
|---|---|---|
algorithm | Yes | Must be hmac-sha256. |
headers | Yes | Must be date. |
signature | Yes | Base64-encoded HMAC-SHA256 signature. |
apikey | Yes | Your public API key. |
productcode | Recommended | Product code/name. Sent automatically by the SDK. Used by the API to resolve subscription context for endpoints like GetSubscriptions. |
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 |
|---|---|---|
Status | string | Business status such as Active, Expired, or NoSeatsAvailable. |
StatusCode | int | API business status code returned inside the response body. |
Description | string? | Human-readable explanation. |
LicenseKey | string? | Activation/license key. |
ProductCode | string? | Product code/name associated with the request. |
HardwareId | string? | 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. |
UserName | string? | Optional user name stored with the seat. |
ComputerName | string? | Optional client computer name. |
AutodeskId | string? | Autodesk account ID associated with the activation, if provided. |
CustomId | string? | Custom identifier associated with the activation, if provided. |
ExpiryDate | DateTime? | Subscription expiry date when available. |
CurrentSeats | int | Currently active seat count. |
MaxSeats | int | Maximum seats allowed by the subscription. |
IsFloating | bool | Whether the subscription uses floating license behavior. |
LastActivated | DateTime? | Last activation or heartbeat timestamp. |
CompanyName | string? | Company name from the subscription record. |
Email | string? | Customer email from the subscription record. |
FullName | string? | Customer full name from the subscription record. |
UserData1 | string? | Custom data field 1 from the subscription. |
UserData2 | string? | Custom data field 2 from the subscription. |
EnabledFeatures | List<string>? | List of feature codes enabled for this subscription (e.g. ["pro", "lte"]). Parsed from the subscription's enabledFeatures JSON field. |
LatestVersion | string? | Latest available application version string (e.g. "2.1.0"). Looked up from the AppVersions table by product code. |
IsSuccess | bool | SDK convenience property. True when StatusCode == 200. |
IsActive | bool | SDK 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.
| Property | Type | Description |
|---|---|---|
ActivationKeys | List<string>? | One or more activation keys to look up (max 20). Fastest query path. |
Email | string? | Exact-match filter on email address. |
CompanyName | string? | Exact-match filter on company name. |
ExpiryDateFrom | DateTime? | Return subscriptions expiring on or after this date (inclusive). |
ExpiryDateTo | DateTime? | Return subscriptions expiring on or before this date (inclusive). |
UserData1 | string? | Exact-match filter on custom user data field 1. |
UserData2 | string? | Exact-match filter on custom user data field 2. |
PageSize | int? | Maximum results per page. Defaults to 100, server max is 1000. |
ContinuationToken | string? | Opaque token from a previous response to fetch the next page. |
GetSubscriptionsResponse
Paginated response returned by GetSubscriptionsAsync.
| Property | Type | Description |
|---|---|---|
Subscriptions | List<GetSubscriptionResponse> | The matching subscriptions for this page. |
Count | int | Number of subscriptions in this page. |
ContinuationToken | string? | Opaque token for fetching the next page. Null when no more results. |
HasMore | bool | Convenience property. True when ContinuationToken is not null. |
GetSubscriptionResponse
Individual subscription record. Returned as items within GetSubscriptionsResponse.Subscriptions.
| Property | Type | Description |
|---|---|---|
ProductName | string? | Product name (partition key). |
ActKey | string? | Activation key (row key). |
CompanyName | string? | Company name. |
FullName | string? | Customer contact name. |
Email | string? | Customer email. |
OrderDate | DateTime | Original order date. |
SubExpiryDate | DateTime | Subscription expiry date. |
LastAccessedDate | DateTime | Date the subscription was last accessed. |
NumberOfLicenses | int | Maximum seat count. |
Disabled | bool | Whether the subscription is disabled. |
IsFloating | bool | Whether floating licenses are enabled. |
UserData1 | string? | Custom data field 1. |
UserData2 | string? | Custom data field 2. |
EnabledFeatures | List<string>? | Enabled feature codes for this subscription. |
UseNamedUsers | bool | Whether the subscription restricts activations to a pre-approved named user list. |
ConsumeFeatureResponse
Returned by ConsumeFeatureAsync after consuming a metered feature.
| Property | Type | Description |
|---|---|---|
Status | string | Business status such as OK or LimitExceeded. |
StatusCode | int | API business status code (200 for success, 409 for limit exceeded). |
Description | string? | Human-readable explanation. |
FeatureCode | string | The feature code that was consumed. |
CurrentCount | int | Total consumption count after this operation. |
MaxConsumptions | int | Maximum allowed consumptions for this feature. |
Remaining | int | Remaining consumptions before the limit is reached. |
IsOverage | bool | Whether the current count exceeds the base limit (overage territory). |
LastConsumedDate | DateTime | Timestamp of the most recent consumption. |
IsSuccess | bool | SDK convenience property. True when StatusCode == 200. |
IsLimitExceeded | bool | SDK convenience property. True when StatusCode == 409. |
ConsumptionStatusResponse
Returned by GetConsumptionStatusAsync. Contains consumption status for one or more features.
| Property | Type | Description |
|---|---|---|
Status | string | Business status. |
StatusCode | int | API status code (200 for success). |
LicenseKey | string | The license key queried. |
Features | List<FeatureConsumptionStatus> | Consumption status for each matching feature. |
IsSuccess | bool | SDK convenience property. True when StatusCode == 200. |
FeatureConsumptionStatus
Individual feature consumption record within a ConsumptionStatusResponse.
| Property | Type | Description |
|---|---|---|
FeatureCode | string | The feature code identifier. |
FeatureName | string | Human-readable feature name. |
CurrentCount | int | Total consumption count in the current period. |
MaxConsumptions | int | Maximum consumptions allowed per period. |
Remaining | int | Remaining consumptions before the limit. |
AllowOverages | bool | Whether overages beyond the limit are permitted. |
MaxOverages | int | Maximum overage units allowed (when overages are enabled). |
IsOverage | bool | Whether consumption has exceeded the base limit. |
ResetPeriod | string | The reset cadence (e.g. "Monthly", "None"). |
LastConsumedDate | DateTime? | When the feature was last consumed. |
LastResetDate | DateTime? | When the consumption counter was last reset. |
KclmSubscriptionResponse
Returned by GetLMSubscriptionAsync. Contains KCLM subscription details with API usage history.
| Property | Type | Description |
|---|---|---|
Subscription | KclmSubscriptionDto | Subscription details including company name, expiry, tier, and limits. |
ApiCallHistory | List<KclmApiCallHistoryPoint> | Monthly API call history for the last 6 months. |
ChartImageMimeType | string | MIME type of the chart image (e.g. "image/png"). |
ChartImageBase64 | string | Base64-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 |
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 |
|---|---|
| Algorithm | AES-256-GCM (authenticated encryption) |
| Key derivation | PBKDF2 with SHA-512, 100,000 iterations |
| Nonce | 12 bytes, cryptographically random per encryption |
| Salt | 16 bytes, cryptographically random per encryption |
| Auth tag | 16 bytes (GCM authentication tag for tamper detection) |
| Stored format | Base64 of [salt 16B][nonce 12B][tag 16B][ciphertext] |
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);
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();
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.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key. |
productCode | Yes | Product name/code. Stored as the subscription partition key. |
hardwareId | Yes | Unique 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). |
userName | No | User name associated with the activation. |
computerName | No | Computer name associated with the activation. |
autodeskId | No | Autodesk account ID associated with the activation. Useful for tracking activations by Autodesk user identity. |
customId | No | Custom 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 Parameter | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key. |
productCode | Yes | Product name/code. |
hardwareId | Yes | Hardware 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.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key. |
hardwareId | Yes | Hardware 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.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key. |
hardwareId | Yes | Hardware 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 Parameter | Required | Description |
|---|---|---|
activationKeys | No | Comma-separated activation keys to look up (max 20). Most efficient query path. |
email | No | Exact-match filter on email address. |
companyName | No | Exact-match filter on company name. |
expiryDateFrom | No | Return subscriptions expiring on or after this date (ISO 8601, inclusive). |
expiryDateTo | No | Return subscriptions expiring on or before this date (ISO 8601, inclusive). |
userData1 | No | Exact-match filter on custom user data field 1. |
userData2 | No | Exact-match filter on custom user data field 2. |
pageSize | No | Maximum results per page. Defaults to 100, max 1000. |
continuationToken | No | Opaque 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
}
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 Parameter | Required | Description |
|---|---|---|
actKey | Yes | KCLM 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.
| Field | Required | Description |
|---|---|---|
productName | Yes | Product name stored as subscription partition key. |
actKey | Yes | Activation key stored as subscription row key. |
companyName | No | Customer company name. |
email | No | Customer email. |
fullName | No | Customer contact name. |
numberOfLicenses | No | Seat count. Set explicitly for useful subscriptions. |
subExpiryDate | No | Subscription expiry date. |
isFloating | No | Whether the license is floating. |
userData1 | No | Custom data field. |
userData2 | No | Custom data field. |
useNamedUsers | No | Whether 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.
| Field | Required | Description |
|---|---|---|
productName | Yes | Product name identifying the subscription (partition key). |
actKey | Yes | Activation key identifying the subscription (row key). |
companyName | No | Updated company name. |
email | No | Updated customer email. |
fullName | No | Updated customer contact name. |
numberOfLicenses | No | Updated seat count. |
subExpiryDate | No | Updated subscription expiry date. |
userData1 | No | Updated custom data field. |
userData2 | No | Updated custom data field. |
useNamedUsers | No | Whether 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.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key identifying the subscription. |
featureCode | Yes | The feature code to consume (must be a consumable/metered feature). |
quantity | No | Number 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.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key identifying the subscription. |
featureCode | No | Specific 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 Code | Meaning | Typical Cause |
|---|---|---|
| 200 | Success | Request authenticated and processed. |
| 400 | Bad Request | Missing body fields, query parameters, or productcode could not be resolved. |
| 401 | Unauthorized | Missing date, invalid API key, signature mismatch, unsupported auth header, or clock skew failure. |
| 404 | Not Found | Subscription not found for the given product name and activation key. |
| 409 | Conflict | Activation request authenticated, but business rules prevented activation. |
| 429 | Too Many Requests | Monthly API call limit exceeded for the subscription tier. |
| 500 | Server Error | Unexpected server-side error. |
LicenseResponse Status Values
| Status | StatusCode | Meaning |
|---|---|---|
Active | 200 | Seat activated successfully. |
AlreadyActive | 200 | This hardware ID already had an active seat; timestamp was refreshed. |
Inactive | 204 | Check completed; the seat is not currently active. |
Blacklisted | 402 | The hardware ID is blacklisted. |
Expired | 503 | The subscription expiry date has passed. |
Disabled | 504 | The subscription is disabled. |
NoSeatsAvailable | 502 | All allowed seats are already active. |
NotFound | 501 | The subscription or license key could not be found. |
Deactivated | 200 | Seat was released successfully. |
OK | 200 | Heartbeat was recorded. |
ErrorResponse Shape
{
"error": "Missing Date header.",
"code": 401,
"details": null
}
Rate Limits
| Tier Number | Monthly Calls |
|---|---|
| 0 | 1,000 |
| 1 | 10,000 |
| 2 | 100,000 |
| 3 | 1,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:
| Platform | Common Option |
|---|---|
| Windows | HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid |
| macOS | IOPlatformUUID or another stable device identifier. |
| Linux | /etc/machine-id or /var/lib/dbus/machine-id. |
| Node.js | Use 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
CheckAsyncat startup or periodically for read-only validation. - Use
HeartbeatAsyncfor floating licenses while the application is running. - Call
DeactivateAsyncwhen a user explicitly releases or transfers the seat. - Use
GetSubscriptionsAsyncto query subscription details with optional filters. Pass no filter to retrieve all subscriptions for the product. - Check
EnabledFeaturesin the activation response to gate product functionality. - Check
LatestVersionin the activation response to prompt users about available updates. - Use
ConsumeFeatureAsyncto track metered feature usage and checkIsLimitExceededbefore granting access. - Use
GetConsumptionStatusAsyncto display remaining quotas to users and handle overage scenarios. - Handle business statuses gracefully; not every license failure is an exception.