KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Advanced

Offline mode and grace

Configure real SDK cache and grace behavior, test connection failures safely and understand revocation and clock limitations.

On this page
Updated 2026-10-21 UTCDownload Markdown

Allow bounded offline use only from a verified cache, never as a fallback after the server rejects a license.

What you will learn

  • Configure product validation/grace policy and all four SDK clients.
  • Distinguish cached, online, offline and refused states.
  • Test expiry, clock rollback and server rejection.

Prerequisites

Policy and state flow

No token requires activation. Verified cache is used before the next validation deadline. After that, online acceptance refreshes state, transport failure can use bounded grace, and server rejection refuses access.
No token requires activation. Verified cache is used before the next validation deadline. After that, online acceptance refreshes state, transport failure can use bounded grace, and server rejection refuses access.
Product settingDefaultDashboard accepted rangeMeaning
validation_interval_hours24 hours1–168 hoursnext_validation_at = server_time + this duration
offline_grace_hours168 hours0–720 hoursoffline_grace_until = server_time + this duration

These are product settings, not constructor parameters. Open Products, edit the product's validation policy, save and validate online to obtain new signed deadlines. Existing disconnected caches retain their old signed deadlines.

Grace is not added after the interval. With defaults, both deadlines start at server_time: online revalidation is due after 24 hours and transport-failure fallback ends after 168 hours. License expiry can end access sooner.

StateActual SDK behaviorUser-facing approach
No activation tokenNOT_ACTIVATED; online activation is requiredAsk for the customer's license key
Verified cache before next_validation_atReturn source=cache without a validation callContinue authorized features
Online acceptanceVerify response and refresh signed stateContinue; show no unnecessary technical message
Transport failure within grace/expiryReverify cached state; source=offlineExplain temporary offline use and reconnect deadline
Server rejectionClear signed state; do not use graceExplain the actual refusal and offer support
Missing/tampered cacheOFFLINE_CACHE_MISSING or OFFLINE_CACHE_INVALIDAsk the user to reconnect
Grace endedOFFLINE_GRACE_EXPIREDAsk the user to connect to continue
Clock more than5 minutes behind latest checkpointCLOCK_ROLLBACK_DETECTEDAsk the user to correct date/time and reconnect

INVALID_TOKEN, DEVICE_MISMATCH, LICENSE_REVOKED, LICENSE_EXPIRED and LICENSE_CANCELLED also remove the local activation token on server rejection. Not every refusal removes it. Do not reinterpret a rate-limit or permission response as a transport outage.

Suggested product policies

These are integration recommendations, not platform defaults or guarantees.

Product typeStarting interval / graceTrade-off
Offline-friendly desktop24 /168 hoursDefault availability; revocation can take up to the cached allowance to reach a disconnected app
High-value connected desktop1 /24 hoursMore calls and shorter stale-access window
Online SaaS backend1 /0 hoursGate valuable server actions online; add your own explicit fail-closed policy
CLI / build tool24 /72 hoursPredictable build availability; avoid logging credentials
Customer-hosted plugin24 /168 hoursUser controls the host; protect truly valuable operations on your server

Zero grace removes transport fallback after the signed deadline, but it is not a guarantee of a network request for every action. check() can use a cache before next_validation_at. validate()/force-online bypasses that shortcut but can still fall back on transport failure within allowed grace. For an online-only action, require an accepted online result and your own server authorization.

Cache and storage

All four clients namespace state by origin, product and environment. They persist installation.json, an activation credential and signed-state.json. Signed state includes version 1, signed_license, verification_key and last_seen_at. Public keys are not secrets; trusting a mutable cache requires your application's trust policy. Do not copy a whole cache between users/installations.

The checkpoint is a local mutable file, not a trusted hardware clock. Cached revocation knowledge cannot appear while disconnected. Known canonicalization/cache discrepancies are described in the verification overview; run your actual payload/cache round trips before shipping.

Complete SDK configuration

Install SDK1.0.0 using the verified quickstart instructions. Save the following files from the archive's configuration directory beside the installed SDK/package. Set KDP_APP_ID, KDP_STATE_DIR and, only for activation, KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL through your secure local environment. KDP_PUBLIC_KEY_FILE optionally points to an approved pin. No interval/grace constructor option exists.

Run each program with activate, check, validate or deactivate. Expected successful output includes valid=true and the actual source; failure exits 1 and keeps feature access false. Do not print or share raw tokens.

Python configuration

For current applications use Python 3.11–3.14 and kdpika-licensing 1.1.5. The adjacent low-level configuration sample retains a deprecated compatibility import; do not treat legacy source requirements as current SDK installation instructions. Run python configuration/python.py check.

python.pypython
"""Full SDK configuration and guarded features. Python3.9+, SDK1.0.0.
Run: python python.py activate|check|validate|deactivate; credentials in KDP_*.
Output: valid/source/code/pdf_export/seat_limit; never credentials or customer data.
"""
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
from license_sdk import LicenseClient

try:
    key_path = os.getenv('KDP_PUBLIC_KEY_FILE')
    client = LicenseClient(
        product=os.environ['KDP_APP_ID'], environment=os.getenv('KDP_ENVIRONMENT', 'SANDBOX'),
        base_url=os.getenv('KDP_BASE_URL', 'https://licensing.kdpika.com'),
        application_name='KDPika configuration example', application_version='1.0.0',
        verification_key=json.loads(Path(key_path).read_text()) if key_path else None,
        storage_dir=Path(os.environ['KDP_STATE_DIR']), timeout=15.0,
        prefer_keyring=os.getenv('KDP_PREFER_KEYRING', 'true') == 'true',
        transport=None, clock=lambda: datetime.now(timezone.utc))
    action = sys.argv[1] if len(sys.argv) == 2 else 'check'
    if action == 'activate':
        result = client.activate(os.environ['KDP_LICENSE_KEY'], os.getenv('KDP_CUSTOMER_EMAIL'))
    elif action == 'validate': result = client.validate()
    elif action == 'deactivate': result = client.deactivate()
    elif action == 'check': result = client.check(force_online=False)
    else: raise ValueError('Unknown action')
    # A feature helper by itself is not an access check.
    enabled = result.valid and result.feature_value('pdf_export', False) is True
    raw_limit = result.feature_value('seat_count', 0) if result.valid else 0
    limit = raw_limit if type(raw_limit) is int and raw_limit >= 0 else 0
    print(json.dumps({'valid':result.valid,'source':result.source,'code':result.error_code,
                      'pdf_export':bool(enabled),'seat_limit':limit}))
    sys.exit(0 if result.valid else 1)
except Exception:
    print('Configuration or verification failed. Check local settings.', file=sys.stderr)
    sys.exit(1)

JavaScript configuration

Node22+, installed local @kdpika/license-sdk1.0.0. Run node configuration/javascript.mjs check. Electron uses its main process.

javascript.mjsjavascript
// Full SDK configuration and guarded features. Node22+, SDK1.0.0 local package.
// Run: node javascript.mjs activate|check|validate|deactivate; KDP_* env values.
// Output safe result fields, never tokens. No automatic safeStorage integration.
import fs from 'node:fs';
import { LicenseClient } from '@kdpika/license-sdk';
try {
  const client = new LicenseClient({
    product:process.env.KDP_APP_ID, environment:process.env.KDP_ENVIRONMENT || 'SANDBOX',
    baseUrl:process.env.KDP_BASE_URL || 'https://licensing.kdpika.com',
    applicationName:'KDPika configuration example', applicationVersion:'1.0.0',
    verificationKey:process.env.KDP_PUBLIC_KEY_FILE ? JSON.parse(fs.readFileSync(process.env.KDP_PUBLIC_KEY_FILE,'utf8')) : null,
    storageDir:process.env.KDP_STATE_DIR, timeoutMs:15000, fetchImpl:globalThis.fetch,
    clock:() => new Date()
  });
  const action = process.argv[2] || 'check'; let result;
  if (action === 'activate') result = await client.activate(process.env.KDP_LICENSE_KEY,process.env.KDP_CUSTOMER_EMAIL || null);
  else if (action === 'validate') result = await client.validate();
  else if (action === 'deactivate') result = await client.deactivate();
  else if (action === 'check') result = await client.check(false);
  else throw Error('Unknown action');
  const enabled = result.valid && result.featureValue('pdf_export',false) === true;
  const rawLimit = result.valid ? result.featureValue('seat_count',0) : 0;
  const limit = Number.isSafeInteger(rawLimit) && rawLimit >= 0 ? rawLimit : 0;
  console.log(JSON.stringify({valid:result.valid,source:result.source,code:result.errorCode,pdf_export:Boolean(enabled),seat_limit:limit}));
  process.exitCode = result.valid ? 0 : 1;
} catch { console.error('Configuration or verification failed. Check local settings.'); process.exitCode = 1; }

PHP configuration

PHP8.2+, cURL and native Sodium. Set KDP_SDK_DIR to the extracted PHP SDK directory. Run php configuration/php.php check.

PHP8.5 can emit a known curl_close deprecation from SDK1.0.0 when error display is enabled. Keep production diagnostics in private logs, not public responses. The recorded local JSON-output test excludes E_DEPRECATED without changing the SDK; it does not ignore other errors.

php.phpphp
<?php
// Full configuration/guarded features. PHP8.2+, cURL/Sodium, SDK1.0.0 direct source.
// Run: php php.php activate|check|validate|deactivate. KDP_SDK_DIR points to SDK.
// Output safe result fields only. Credentials use KDP_* environment variables.
declare(strict_types=1);
try {
    $sdk = rtrim((string)getenv('KDP_SDK_DIR'), '/');
    require $sdk . '/src/LicenseResult.php'; require $sdk . '/src/LicenseClient.php';
    $keyFile = getenv('KDP_PUBLIC_KEY_FILE');
    $client = new KDPika\LicenseSdk\LicenseClient(
        product:(string)getenv('KDP_APP_ID'), environment:getenv('KDP_ENVIRONMENT') ?: 'SANDBOX',
        baseUrl:getenv('KDP_BASE_URL') ?: 'https://licensing.kdpika.com',
        applicationName:'KDPika configuration example', applicationVersion:'1.0.0',
        verificationKey:$keyFile ? json_decode((string)file_get_contents($keyFile),true,64,JSON_THROW_ON_ERROR) : null,
        storageDir:(string)getenv('KDP_STATE_DIR'), timeout:15.0, transport:null,
        clock:static fn(): DateTimeImmutable => new DateTimeImmutable('now',new DateTimeZone('UTC')));
    $action = $argv[1] ?? 'check';
    $result = match($action) {
        'activate' => $client->activate((string)getenv('KDP_LICENSE_KEY'),getenv('KDP_CUSTOMER_EMAIL') ?: null),
        'check' => $client->check(false), 'validate' => $client->validate(), 'deactivate' => $client->deactivate(),
        default => throw new RuntimeException('Unknown action')
    };
    $enabled = $result->valid && $result->featureValue('pdf_export',false) === true;
    $rawLimit = $result->valid ? $result->featureValue('seat_count',0) : 0;
    $limit = is_int($rawLimit) && $rawLimit >= 0 ? $rawLimit : 0;
    echo json_encode(['valid'=>$result->valid,'source'=>$result->source,'code'=>$result->errorCode,'pdf_export'=>(bool)$enabled,'seat_limit'=>$limit],JSON_THROW_ON_ERROR) . PHP_EOL;
    exit($result->valid ? 0 : 1);
} catch (Throwable) { fwrite(STDERR,"Configuration or verification failed. Check local settings.\n"); exit(1); }

C# configuration

.NET7+, local SDK1.0.0 project. Build with dotnet build configuration/Configuration.csproj -p:KDPikaSdkProject=/absolute/path/KDPika.LicenseSdk.csproj -o build/configuration, then dotnet build/configuration/Configuration.dll check. Quote paths containing spaces.

Configuration.csprojxml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="$(KDPikaSdkProject)" />
  </ItemGroup>
</Project>
Program.cscsharp
// Full SDK configuration/guarded features. .NET7+, SDK1.0.0 project reference.
// Run built Configuration.dll activate|check|validate|deactivate. KDP_* env values.
// Output safe result fields only. Native DPAPI is not built into this SDK.
using System.Text.Json;
using KDPika.LicenseSdk;
try {
    string Required(string name) => Environment.GetEnvironmentVariable(name) ?? throw new ArgumentException(name);
    string Optional(string name,string fallback) => Environment.GetEnvironmentVariable(name) ?? fallback;
    var keyPath = Environment.GetEnvironmentVariable("KDP_PUBLIC_KEY_FILE");
    using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
    var client = new LicenseClient(
        product:Required("KDP_APP_ID"), environment:Optional("KDP_ENVIRONMENT","SANDBOX"),
        baseUrl:Optional("KDP_BASE_URL","https://licensing.kdpika.com"),
        applicationName:"KDPika configuration example", applicationVersion:"1.0.0",
        verificationKey:keyPath is null ? null : JsonSerializer.Deserialize<Dictionary<string,string>>(File.ReadAllText(keyPath)),
        storageDir:Required("KDP_STATE_DIR"), httpClient:http, clock:() => DateTimeOffset.UtcNow);
    var result = (args.FirstOrDefault() ?? "check") switch {
        "activate" => await client.ActivateAsync(Required("KDP_LICENSE_KEY"),Environment.GetEnvironmentVariable("KDP_CUSTOMER_EMAIL")),
        "check" => await client.CheckAsync(false), "validate" => await client.ValidateAsync(),
        "deactivate" => await client.DeactivateAsync(), _ => throw new ArgumentException("Unknown action")
    };
    var enabled = result.Valid && result.Feature("pdf_export",false);
    var rawLimit = result.Valid ? result.FeatureValue("seat_count") : null;
    var limit = rawLimit is JsonElement value && value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number) && number >= 0 ? number : 0;
    Console.WriteLine(JsonSerializer.Serialize(new {valid=result.Valid,source=result.Source,code=result.ErrorCode,pdf_export=enabled,seat_limit=limit}));
    return result.Valid ? 0 : 1;
} catch { Console.Error.WriteLine("Configuration or verification failed. Check local settings."); return 1; }

Test offline behavior

  1. Activate the test installation online. Confirm valid=true and source=online.
  2. Run check immediately. Confirm source=cache if the saved canonical state verifies.
  3. Block only this app's network in your local test transport; do not change production DNS or server settings.
  4. Advance an injected test clock past next_validation_at but before both expiry and offline_grace_until. Confirm source=offline and valid=true.
  5. Advance beyond offline_grace_until. Confirm OFFLINE_GRACE_EXPIRED and no feature access.
  6. Reset only the isolated test scenario, then move its clock more than5 minutes behind the checkpoint. Confirm CLOCK_ROLLBACK_DETECTED.
  7. Restore network and suspend/revoke only the synthetic Sandbox license. Force validation and confirm refusal, not offline fallback.
  8. Inspect the local state and confirm signed state is removed after server rejection.
  9. Deactivate or release the test installation when finished.

Expected result: connection failure and explicit refusal produce different outcomes. Do not accelerate a real machine's clock or delete a real customer's identity to run this test.

Expected result

A disconnected app continues only with authentic, correctly bound, unexpired cached state inside its allowed grace. Rejection never unlocks features.

Common mistakes

  • Adding grace after next_validation_at instead of using the signed absolute deadline.
  • Assuming force-online disables offline fallback.
  • Treating a successful feature helper on an invalid result as authorization.

Security notes

SDK clock/transport injection is for deterministic tests. Never let an untrusted user select a trusted production clock. File permissions and vaults reduce token exposure but cannot stop a local administrator from patching the client.

Troubleshooting

SymptomCauseAction
Offline fails immediatelyMissing/unverifiable cache or expired deadlineReconnect and validate; do not ignore signature errors
Revoked license still works while disconnectedOld signed snapshot remains inside graceUse a shorter product policy or server-side features
Policy edit appears ineffectiveCached deadlines came from an earlier responseValidate online and inspect the new signed deadlines

Next steps

Read signed responses and the threat model, then validate your own integration in Sandbox.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings