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 Project KCLicenseManager.Sdk
API Project KCLicenseManagerAPI10
SDK Targets .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.

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

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 Response License, Subscription, or Error response 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"

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 and signature in the Authorization header.
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.
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
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?Stable client machine identifier.
UserNamestring?Optional user name stored with the seat.
ComputerNamestring?Optional client computer name.
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.
IsSuccessboolSDK convenience property. True when StatusCode == 200.
IsActiveboolSDK 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);
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.

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.

FieldRequiredDescription
licenseKeyYesActivation/license key.
productCodeYesProduct name/code. Stored as the subscription partition key.
hardwareIdYesStable identifier for this machine or seat.
userNameNoUser name associated with the activation.
computerNameNoComputer 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 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.

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.
[
  {
    "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.

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.
{
  "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 CodeMeaningTypical Cause
200SuccessRequest authenticated and processed.
400Bad RequestMissing body fields or query parameters.
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 (update endpoint).
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 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.

PlatformCommon Option
WindowsHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
macOSIOPlatformUUID 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 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.
  • Handle business statuses gracefully; not every license failure is an exception.