---
title: "C# SDK"
description: "Install and configure the official C# SDK with exact members, safe lifecycle examples, storage and release limitations."
slug: "/docs/v1/sdks/csharp"
section: "SDKs and languages"
order: 50
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart", "/docs/v1/security/offline-mode"]
draft: false
---
Integrate the unchanged C# SDK with an explicit validity gate and a stable private installation state.

## What you will learn
- Install the actual source package and configure every constructor option.
- Use the complete session CLI and distinguish release success from [feature](/docs/v1/start/glossary#feature) access.
- Understand every exposed member, storage/error/offline boundary and packaging limit.

## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [signed responses](/docs/v1/security/signed-responses) and use only synthetic Sandbox data.

## Runtime and package
Project targets net7.0 with BouncyCastle.Cryptography2.7.0. Direct ProjectReference is verified; no NuGet registry release is established. Build with a compatible, maintained SDK/runtime and test the actual target application.
Package KDPika.LicenseSdk, current label 1.0.0. Download kdpika-csharp-sdk-1.0.0.zip from the SDK overview and verify SHA-256 before extracting. This is a local source install, not a claim that a public registry release exists.

## Install and use
Run the installation command in an empty application project with the extracted SDK directory. Set KDP_APP_ID and a private KDP_STATE_DIR. For PHP also set KDP_SDK_DIR to the extracted package directory. For [activation](/docs/v1/start/glossary#activation) set KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL from trusted user input/your secret environment, not source literals.
```bash
dotnet add reference ./kdpika-csharp-sdk/KDPika.LicenseSdk/KDPika.LicenseSdk.csproj
dotnet restore
```
Expected: the exact local SDK installs/builds and its imports resolve. Python on Windows uses .venv\Scripts\python.exe instead of .venv/bin/python; the transcript is the tested macOS path. No registry release is implied by dependency restoration.
The session program is complete and runs one explicit action per invocation, preserving state across processes. Its SDK constructor deliberately sets SANDBOX. The key-prefix/URL checks are demonstration guards, not a replacement for server environment lookup or SDK verification. HTTPS is the default; local fixture HTTP requires KDP_ALLOW_LOOPBACK=1.
```csharp
// Sandbox session CLI, .NET7+ and SDK1.0.0 extracted beside Session.csproj.
// Run dotnet run --project Session.csproj -- status|activate|check|validate|deactivate.
// Set KDP_APP_ID,KDP_STATE_DIR; activate: KDP_LICENSE_KEY/optional customer email.
// Expected safe JSON decisions, not a token/raw payload. Keep the Valid guard.
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using KDPika.LicenseSdk;

static string Required(string name){var value=Environment.GetEnvironmentVariable(name)?.Trim();return string.IsNullOrEmpty(value)?throw new ArgumentException("Missing configuration"):value;}
try{
    var action=args.Length==1?args[0]:"";
    if(!new[]{"status","activate","check","validate","deactivate"}.Contains(action))throw new ArgumentException("Choose an action");
    var origin=(Environment.GetEnvironmentVariable("KDP_BASE_URL")??"https://licensing.kdpika.com").TrimEnd('/');
    var uri=new Uri(origin);var local=uri.Scheme=="http"&&new[]{"127.0.0.1","localhost"}.Contains(uri.Host)&&Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK")=="1";
    if(!string.IsNullOrEmpty(uri.UserInfo)||uri.AbsolutePath!="/"||!string.IsNullOrEmpty(uri.Query)||!string.IsNullOrEmpty(uri.Fragment)||(uri.Scheme!="https"&&!local))throw new ArgumentException("Use an HTTPS site origin");
    var client=new LicenseClient(Required("KDP_APP_ID"),"SANDBOX",origin,applicationName:"KDPika SDK session example",
        applicationVersion:"1.0.0",storageDir:Required("KDP_STATE_DIR"));
    if(action=="status"){
        Console.WriteLine(JsonSerializer.Serialize(new{activated=client.Activated,storage_security="permission-protected-file",
            installation_hash=Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(client.InstallationId))).ToLowerInvariant()}));return;
    }
    LicenseResult result;
    if(action=="activate"){
        var key=Required("KDP_LICENSE_KEY");if(!Regex.IsMatch(key,"^TEST[A-Z0-9]*-",RegexOptions.IgnoreCase))throw new ArgumentException("Use a Sandbox key");
        result=await client.ActivateAsync(key,Environment.GetEnvironmentVariable("KDP_CUSTOMER_EMAIL"));
    }else result=action switch{"check"=>await client.CheckAsync(),"validate"=>await client.ValidateAsync(),_=>await client.DeactivateAsync()};
    var active=action!="deactivate"&&result.Valid&&result.SignatureValid;
    var feature=active?result.FeatureValue("seat_count"):null;
    int? limit=feature is JsonElement element&&element.ValueKind==JsonValueKind.Number&&element.TryGetInt32(out var number)&&number>=0?number:null;
    Console.WriteLine(JsonSerializer.Serialize(new{valid=result.Valid,source=result.Source,error_code=result.ErrorCode,
        signature_valid=result.SignatureValid,pdf_export_allowed=active&&result.Feature("pdf_export"),seat_count=limit,
        request_id=Regex.IsMatch(result.RequestId??"","^req_[a-f0-9]{24}$")?result.RequestId:null,
        message=action=="deactivate"&&result.Valid?"This installation is deactivated.":result.Valid?"Your license is ready.":"Your license could not be confirmed. Check your key or contact the software publisher."}));
    Environment.ExitCode=result.Valid?0:1;
}catch{
    Console.Error.WriteLine("License check unavailable. Review configuration, connectivity and private storage permissions. Do not disable signature verification.");Environment.ExitCode=2;
}
```

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType><TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable>
    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
  </PropertyGroup>
  <ItemGroup><Compile Include="Program.cs" />
    <ProjectReference Include="kdpika-csharp-sdk/KDPika.LicenseSdk/KDPika.LicenseSdk.csproj" />
  </ItemGroup>
</Project>
```
1. Run dotnet run --project Session.csproj -- status. Expected: activated=false for fresh storage, with only an installation hash and storage category.
2. Run dotnet run --project Session.csproj -- activate with a Sandbox key and matching email. Expected: valid=true/signature_valid=true or an explicit refusal, never a raw token/payload dump.
3. Run dotnet run --project Session.csproj -- check using the same state directory. Expected: current valid result from the eligible cache or online path; no feature is enabled on failure.
4. Run dotnet run --project Session.csproj -- validate. Expected: forced-online path, subject to the documented bounded transport-failure fallback.
5. Run dotnet run --project Session.csproj -- deactivate. Expected: the installation seat is released and local token/cache removed; pdf_export_allowed remains false for this release operation.
6. Run dotnet run --project Session.csproj -- status again. Expected: activated=false with the same installation hash. Cumulative activation_count is not reset.
The programs exit0 for an accepted operation/status, exit1 for a licensing refusal, exit2 for configuration/unexpected failure. They expose safe decisions only. seat_count is read only from a verified valid result and must be a nonnegative integer; an absent value remains null. Result feature helpers cannot substitute for the validity/signature gate.

## Complete constructor configuration
| Option | Type | Default | Meaning |
| --- | --- | --- | --- |
| [product](/docs/v1/start/glossary#product) | string | required | [Public App ID](/docs/v1/start/glossary#public-app-id); never the internal product ID or an lmp_ key. |
| environment | string | [LIVE](/docs/v1/start/glossary#live) (Python: production maps to LIVE) | Set SANDBOX explicitly for examples. production/test aliases normalize to LIVE/SANDBOX. |
| baseUrl | string | https://licensing.kdpika.com | Site origin, without /api/v1; default HTTPS. Loopback HTTP is for local testing only. |
| applicationName | string | Licensed Application | Names the default application storage directory and [device](/docs/v1/start/glossary#device) fallback label. |
| applicationVersion | string | empty string | Application release sent at activation. Empty version skips the server version-range check. |
| verificationKey | Dictionary<string,string>? | null/None | Optional pinned key with key_id and raw base64url public_key; manual rotation. |
| storageDir | string? | OS-derived application storage root | Namespace includes origin, product and environment; keep outside public web roots. |
| httpClient | HttpClient? | null creates a 15-second client | An injected client controls its own timeout/handler. Caller manages any injected client lifetime; no built-in token-vault injection. |
| clock | Func<DateTimeOffset>? | current UTC-compatible time | Testing clock; do not expose time control to untrusted application callers. |
Timeout is an HTTP timeout, not a client-side way to configure [offline grace](/docs/v1/start/glossary#offline-grace) or [validation interval](/docs/v1/start/glossary#validation-interval). Those times come from the verified signed payload and the product configuration. Defaults are actual source defaults; the runnable sample explicitly changes environment/storage choices for safe isolated tests.

## Public members and result contract
This inventory includes constructors, fields/properties, lifecycle/result helpers and exposed low-level members. JavaScript helpers are language-public even when they are implementation details. Python non-underscore storage/transport classes are listed rather than silently hidden. The exact source signature is shown so a guide cannot invent a method such as has_feature().
| Member | Kind | Exact signature / field | Use and return/error boundary |
| --- | --- | --- | --- |
| LicenseClient.LicenseClient | constructor | public LicenseClient(string product, string environment = "LIVE", string baseUrl = "https://licensing.kdpika.com", string applicationName = "Licensed Application", string applicationVersion = "", Dictionary<string,string>? verificationKey = null, string? storageDir = null, HttpClient? httpClient = null, Func<DateTimeOffset>? clock = null) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseClient.ActivateAsync | method | public async Task<LicenseResult> ActivateAsync(string licenseKey,string? email=null,CancellationToken cancellationToken=default) | Send activation; return a valid signed result or explicit refusal. Success stores the token and signed state; verification failure attempts best-effort deactivation. |
| LicenseClient.CheckAsync | method | public async Task<LicenseResult> CheckAsync(bool forceOnline=false,CancellationToken cancellationToken=default) | Use verified cache when eligible or validate online. A transport failure can enter bounded offline policy; explicit server rejection is not offline permission. |
| LicenseClient.ValidateAsync | method | public Task<LicenseResult> ValidateAsync(CancellationToken cancellationToken=default) | Force the check path online; transport failure can still attempt bounded verified offline fallback, not an unconditional online-only guarantee. |
| LicenseClient.DeactivateAsync | method | public async Task<LicenseResult> DeactivateAsync(CancellationToken cancellationToken=default) | Release this token seat online and clear token/cache on success. A successful release result does not grant access. Installation identity remains. |
| LicenseClient.InstallationId | property | public string InstallationId | Persisted random installation identity; not a hardware fingerprint or secret [developer API key](/docs/v1/start/glossary#developer-api-key). |
| LicenseClient.Activated | property | public bool Activated | Token-presence status only; not a current entitlement decision. |
| LicenseResult.Feature | method | public bool Feature(string name, bool fallback = false) | Boolean-style feature lookup only. Never authorizes invalid results; C# true/false fallback semantics differ from truthiness in the other SDKs. |
| LicenseResult.FeatureValue | method | public JsonElement? FeatureValue(string name) | Read the typed feature value/default. Check valid and the actual feature type first. |
| LicenseResult.LicenseResult | constructor | public sealed record LicenseResult(bool Valid, string Source, JsonElement Payload, string? ErrorCode = null, string Message = "", string? RequestId = null, bool SignatureValid = false, string StorageSecurity = "permission-protected-file") | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseResult.Valid | field | bool Valid | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.Source | field | string Source | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.Payload | field | JsonElement Payload | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.ErrorCode | field | string? ErrorCode = null | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.Message | field | string Message = "" | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.RequestId | field | string? RequestId = null | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.SignatureValid | field | bool SignatureValid = false | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseResult.StorageSecurity | field | string StorageSecurity = "permission-protected-file" | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
A LicenseResult carries valid/source/payload/error code/message/[request ID](/docs/v1/start/glossary#request-id)/signature status/storage category under the language-specific spelling above. Lifecycle calls return this result type. Unknown codes fail closed. HttpResponse/custom transport status is not an access grant. A field in a constructed result has not been verified merely because it is present.
The exact runnable session covers the real Sandbox lifecycle and safe output. Representative local/model/helper assertions execute every inventoried public member; the optional Python dialog has four genuine offscreen Qt cases. These are not claims that every branch, injected cancellation, native vault, framework or target OS was executed. The full app examples record actual packaged/native versus cross-build/controller boundaries explicitly.

## Runnable member examples
This separate complete program exercises the public local/result/helper surface with explicit assertions and no access grant. It is an isolated conformance exercise, not an alternative licensing implementation. Run it beside the installed source package using dotnet run --project Members.csproj. Expected: result=PASS and a list of exercised members, without credentials or raw [customer](/docs/v1/start/glossary#customer) payloads.
Python makes one read-only public-key GET to KDP_BASE_URL (default HTTPS production origin); its product is an unusable example App ID, and it does not activate. Its optional PyQt dialog is tested separately in four offscreen cases. PHP and C# use only synthetic local objects and no HTTP. JavaScript uses an ephemeral test signing key and injected synthetic transport, never a real KDPika credential or network response. The normal session example above is separately tested against the real isolated Sandbox.
These examples use their own fresh temporary directories. They never inspect or delete an existing installation. Result fields are constructed as invalid deliberately: a true feature helper cannot override valid=false. Typed return assertions demonstrate behavior, not complete branch, native vault or target-OS coverage.
```csharp
// Isolated local result/member exercise. No payment/API/credential request.
using System.Text.Json;
using KDPika.LicenseSdk;

var covered = new List<string>();
void Check(string member, bool condition) {
    if (!condition) throw new InvalidOperationException("Member example failed: " + member);
    covered.Add(member);
}
var temporary = Path.Combine(Path.GetTempPath(), "kdpika-members-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(temporary);
try {
    var client = new LicenseClient("YOUR_APP_ID", "SANDBOX", storageDir: temporary);
    Check("LicenseClient.LicenseClient", client is not null);
    Check("LicenseClient.InstallationId", client!.InstallationId.StartsWith("lmp-install-", StringComparison.Ordinal));
    Check("LicenseClient.Activated", !client.Activated);
    Check("LicenseClient.ActivateAsync", (await client.ActivateAsync("")).ErrorCode == "INVALID_LICENSE");
    Check("LicenseClient.CheckAsync", (await client.CheckAsync()).ErrorCode == "NOT_ACTIVATED");
    Check("LicenseClient.ValidateAsync", (await client.ValidateAsync()).ErrorCode == "NOT_ACTIVATED");
    Check("LicenseClient.DeactivateAsync", (await client.DeactivateAsync()).ErrorCode == "NOT_ACTIVATED");
    // Constructing this record does NOT verify the payload or grant access.
    var payload = JsonSerializer.SerializeToElement(new { features = new { pdf_export = true, seat_count = 2 } });
    var result = new LicenseResult(false, "example", payload, "EXAMPLE_ONLY", "Example", "req_example", false, "unknown");
    Check("LicenseResult.LicenseResult", result is not null);
    Check("LicenseResult.Valid", !result!.Valid);
    Check("LicenseResult.Source", result.Source == "example");
    Check("LicenseResult.Payload", result.Payload.GetProperty("features").GetProperty("seat_count").GetInt32() == 2);
    Check("LicenseResult.ErrorCode", result.ErrorCode == "EXAMPLE_ONLY");
    Check("LicenseResult.Message", result.Message == "Example");
    Check("LicenseResult.RequestId", result.RequestId == "req_example");
    Check("LicenseResult.SignatureValid", !result.SignatureValid);
    Check("LicenseResult.StorageSecurity", result.StorageSecurity == "unknown");
    Check("LicenseResult.Feature", result.Feature("pdf_export") && !result.Feature("missing"));
    Check("LicenseResult.FeatureValue", result.FeatureValue("seat_count")?.GetInt32() == 2 && result.FeatureValue("missing") is null);
    Check("application.fail_closed", !(result.Valid && result.SignatureValid && result.Feature("pdf_export")));
    Console.WriteLine(JsonSerializer.Serialize(new { result = "PASS", members = covered,
        mode = "synthetic local objects; zero HTTP", access_granted = false }));
} finally {
    // Only this exact fresh scratch directory is removed, never user state.
    Directory.Delete(temporary, recursive: true);
}
```

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="Members.cs" />
    <ProjectReference Include="kdpika-csharp-sdk/KDPika.LicenseSdk/KDPika.LicenseSdk.csproj" />
  </ItemGroup>
</Project>
```

## Errors and exceptions
Constructor configuration errors throw ArgumentException. Async lifecycle calls return LicenseResult for many transport/[license](/docs/v1/start/glossary#license) failures; unexpected storage/JSON errors can still throw. CancellationToken is accepted by every public lifecycle method, but broad catches can map cancellation into a transport/offline result. Do not assume an OperationCanceledException always propagates. Observe the result and your cancellation state before updating UI.
Use [the complete error table](/docs/v1/api/errors) for code/namespace/cause/correction and safe user messages. Log only application version, UTC time, action, code, source and a validated req_ correlation value. Never log the key, token, Authorization header, customer payload or an unredacted state file. No debug flag changes the signature or validity policy.

## Storage and installation identity
Permission-protected per-user application-data files, Unix owner-only modes where supported, not automatic Windows DPAPI/Credential Manager or MAUI SecureStorage. Activated means token presence, not verified validity. Preserve InstallationId and keep files outside the application package; there is no public reset-local method or token-store injection option.
Storage namespace is based on origin/product/environment. Keep those stable across app updates; changing them can create a separate identity/seat. Reinstalling or deleting identity can consume another cumulative activation even after a seat was released. Do not use machine serials/MAC addresses as a substitute for the persistent random ID.

## Offline and clock policy
A new activation requires network. check uses verified signed state before next_validation_at when eligible. After that point it validates online; only a transport failure attempts the verified offline path. Explicit server rejection clears signed cache and selected invalid-token/device/revocation/expiry/cancellation errors also remove the stored credential. Offline/cache paths require active status, valid signature, product/environment/installation binding, expiry/grace and clock policy.
Clock rollback tolerance is300 seconds behind the latest trusted checkpoint/server time. A user controlling the host can patch the program or roll back a full machine snapshot; local checks are not absolute anti-tamper guarantees. Some malformed/missing signed time cases and current canonicalization issues are recorded limitations, not intended feature grants.
Validation interval/offline grace are not constructor options. Configure the product on KDPika, then verify what the signed server_time/next_validation_at/offline_grace_until actually say. Offline grace is based on server_time, not added after next_validation_at. Do not calculate a second extension or treat a failed check as permission.
Read [the full offline guide](/docs/v1/security/offline-mode) and [test vectors](/docs/v1/security/test-vectors). Known Python separator/exponent, JavaScript integral-float and C# cached-Unicode compatibility failures remain unchanged. A successful narrow local lifecycle is not universal canonicalization conformance.

## Threading, asynchronous work and lifetime
Use await on lifecycle methods and pass CancellationToken where appropriate; no synchronous facade or multi-writer state lock is supplied. Serialize operations for one installation. An injected HttpClient retains its configured handler/timeout; do not assume the constructor replaces those settings. No client IDisposable ownership contract is declared.

## Application and framework integration
WPF/WinForms are Windows desktop surfaces; a net7 library does not make their UI cross-platform. Use awaited calls in application commands and marshal state back to the UI. MAUI needs its supported target runtimes and platform secure storage integration, which this SDK does not implement automatically. Console/service callers can await the same lifecycle without UI dependencies. Native GUI execution is not inferred from a macOS console run.

## Packaging and distribution
For a desktop build, package the application, the referenced SDK and pinned Bouncy Castle dependency; choose target runtime/signing deliberately. Never embed a populated cache/token/key. Validate raw server responses and cache reload on the target platform, especially the documented Unicode cache limitation. Do not disable signatures to make a published binary work.

## Upgrade and uninstall
Pin the current source checksum and runtime/dependency lock. Before changing a version, compare constructor/public members, storage namespace, canonical bytes, time policy and errors. Run activation/cache reload/offline/explicit refusal/deactivation on a separate Sandbox installation. No historical version or compatibility window is invented by this guide.
Deactivate while online before intentionally removing a deployed installation. Removing the package alone does not release a server seat or erase your application storage. Retaining storage may preserve identity for a later reinstall; deleting it can create another installation. Follow your application privacy/retention policy rather than recursively deleting a broad user directory.
The language-specific removal command applies only if the package/reference was installed into that application project:
```bash
dotnet remove reference ./kdpika-csharp-sdk/KDPika.LicenseSdk/KDPika.LicenseSdk.csproj
```
For a direct copied PHP source directory, removing a Composer root package is not the same as composer remove in a consuming application. Remove only the exact unused reference/files after backing up required application state; no broad deletion command is prescribed.

## Expected result
Your application handles explicit refusal without enabling features, preserves the installation identity, and deactivates deliberately when releasing a seat. Check the real signed result, not only a transport status.

## Common mistakes
- Using activated/token presence or a feature helper as a validity proof.
- Putting /api/v1 in the SDK site-origin constructor or omitting SANDBOX in a test.
- Deleting identity/cache and assuming a server seat or cumulative activation was reset.
- Suppressing a signature mismatch instead of testing the actual release limitation.

## Security notes
Keep credentials off renderer/browser surfaces, retain TLS/signature verification, and use a verified valid result for the specific access action. A successful deactivate/status operation never grants a feature. Use private storage and safe correlation metadata only.

## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| NOT_ACTIVATED | No stored credential in this namespace | Activate a Sandbox installation; keep origin/product/environment/storage stable |
| INVALID_LICENSE | Wrong key/email/App ID/environment | Correct the input; do not retry unchanged requests blindly |
| SIGNATURE_INVALID or cache rejection | Tampering, trust mismatch or current canonicalization limitation | Fail closed and run real response/cache/vector checks |
| State disappeared after update | Application name/origin/storage namespace changed | Restore the intended stable namespace; reconcile any extra seat |
| Network failure at startup | Activation requires network or cache is unavailable/invalid | Reconnect or show a clear retry/support path without unlocking |

## Next steps
Review [the Sandbox quickstart](/docs/v1/start/quickstart), [signed responses](/docs/v1/security/signed-responses) and [error handling](/docs/v1/api/errors).

## Related
- [Authentication](/docs/v1/start/authentication)
- [Offline policy](/docs/v1/security/offline-mode)
- [Secure token storage](/docs/v1/security/secure-token-storage)
