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.
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 or update existing subscription details via the API.
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, and .NET plugins. | Automatic HMAC signing through HmacMessageHandler. |
| Direct HTTP | Python, 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}");
}
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.
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"
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 and signature in the
Authorizationheader.
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. |
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. |
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. |
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. |
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? | Stable client machine identifier. |
UserName | string? | Optional user name stored with the seat. |
ComputerName | string? | Optional client computer name. |
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. |
IsSuccess | bool | SDK convenience property. True when StatusCode == 200. |
IsActive | bool | SDK convenience property. True for Active or AlreadyActive. |
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);
if (!activation.IsActive)
{
Console.WriteLine($"Activation failed: {activation.Status} - {activation.Description}");
return;
}
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 machine identifier strategy.
return Environment.MachineName;
}
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"
}
});
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)
});
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.
IHttpClientFactory Integration
using KCLicenseManager.Sdk;
using KCLicenseManager.Sdk.Auth;
using KCLicenseManager.Sdk.Models;
builder.Services.AddTransient(_ => new HmacMessageHandler(apiKey, sharedSecret));
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"
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}"'
),
"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",
}
response = requests.post(
f"{BASE_URL}/v2/license/activate",
headers=auth_headers(),
json=payload,
timeout=30,
)
response.raise_for_status()
return response.json()
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())
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",
},
timeout=30,
)
print(update_response.json())
curl Examples
Generate Headers with OpenSSL
API_KEY="kc_pub_your_public_key"
SHARED_SECRET="kc_sec_your_shared_secret"
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}\""
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"
}'
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"
}'
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"
}'
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 stable hardware ID.
| Field | Required | Description |
|---|---|---|
licenseKey | Yes | Activation/license key. |
productCode | Yes | Product name/code. Stored as the subscription partition key. |
hardwareId | Yes | Stable identifier for this machine or seat. |
userName | No | User name associated with the activation. |
computerName | No | Computer name associated with the activation. |
{
"licenseKey": "ACT-KEY-123",
"productCode": "Bonus Tools",
"hardwareId": "MACHINE-GUID-OR-STABLE-ID",
"userName": "Jane Smith",
"computerName": "WORKSTATION-01"
}
Success: HTTP 200 with Status Active or AlreadyActive.
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.
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. |
[
{
"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"
}
]
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. |
{
"productName": "Bonus Tools",
"actKey": "ACT-KEY-001",
"companyName": "Updated Company Name",
"email": "newemail@example.com",
"numberOfLicenses": 10,
"subExpiryDate": "2028-05-06T00:00:00Z"
}
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.
Errors and Status Values
HTTP Status Codes
| HTTP Code | Meaning | Typical Cause |
|---|---|---|
| 200 | Success | Request authenticated and processed. |
| 400 | Bad Request | Missing body fields or query parameters. |
| 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 (update endpoint). |
| 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 and public API key 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
Choose a stable value that survives reboots and normal updates. Avoid IP addresses and other values that change frequently.
| 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. |
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. - Handle business statuses gracefully; not every license failure is an exception.