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

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Intermediate

Validate an installation

Refresh the signed state of an activated installation.

On this page
Updated 2026-10-17 UTCDownload Markdown

Refresh the signed state of an activated installation.

What you will learn

  • Send this exact operation with its required credentials and fields.
  • Interpret success, refusal, side effects and retry risks.

Prerequisites

Method and path

POST /api/v1/licenses/validate

When to use it

Refresh the signed state of an activated installation. Send its activation token and the exact persisted installation ID, then verify the returned envelope and semantic policy before enabling features.

Authentication and environment

Use Authorization: Bearer YOUR_ACTIVATION_TOKEN. The token fixes the installation/product/environment; X-Environment cannot override it.

Request headers

HeaderRequiredDescriptionExample
AcceptrecommendedJSON responsesapplication/json
Content-Typefor JSON bodyUse the documented JSON objectapplication/json
AuthorizationyesInstallation tokenBearer YOUR_ACTIVATION_TOKEN
X-Installation-IDnoAlways send the persisted ID. Omission signs a null installation hash that official clients reject.YOUR_PERSISTED_INSTALLATION_ID

Path and query parameters

No path parameters are accepted.

No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.

Request body

No body fields are consumed. Omit the body or send an empty object.

Full request JSON

jsonjson
{}

The schemas describe supported recommended input types. Current PHP parsing trims/casts strings and may ignore unknown fields; it is not strict JSON-schema validation. Invalid optional descriptive fields can become empty/null. Conventions describes malformed JSON and alternate parsing.

Success response

HTTP 200. The following is a real isolated Sandbox capture; secret values are replaced with explicit placeholders.

jsonjson
{
  "success": true,
  "valid": true,
  "license": {
    "payload": {
      "license_id": "lic_578581bd80c2a672da30fc29",
      "product_id": "app_a642cd24641ec976626b",
      "product": "Docs fixture product",
      "plan": "fixture-plan",
      "status": "active",
      "environment": "sandbox",
      "expires_at": "2026-11-16T10:39:35+00:00",
      "license_key_mask": "TESTDOCS-••••-••••-HAHH",
      "starts_at": "2026-10-17T10:39:35+00:00",
      "created_at": "2026-10-17T12:39:35+00:00",
      "max_devices": 2,
      "max_activations": 4,
      "activation_count": 1,
      "min_version": null,
      "max_version": null,
      "active_devices": 1,
      "customer": {
        "id": "cus_d7db5516f8377f89a869e293",
        "name": "Synthetic contract customer",
        "email": "contract-6b9f0843f1d3609f@example.invalid",
        "company": null,
        "country": null
      },
      "product_details": {
        "id": "app_a642cd24641ec976626b",
        "name": "Docs fixture product",
        "platform": "desktop"
      },
      "plan_details": {
        "id": "plan_3bf54c099326639ee183661f",
        "name": "Fixture plan",
        "slug": "fixture-plan",
        "billing_type": "monthly",
        "price": 19.5,
        "currency": "EUR",
        "duration_days": 30,
        "trial_days": null
      },
      "features": {
        "pdf_export": true
      },
      "device": {
        "id": "dev_299a6753c013047b3ed013de",
        "installation_hash": "e119b615ebfb5e5a2da5ea9c2a64b2117621399ff61d16718d460e0bb919a988",
        "name": "Synthetic local installation",
        "platform": "test",
        "app_version": "1.0.0"
      },
      "server_time": "2026-10-17T10:39:35+00:00",
      "next_validation_at": "2026-10-18T10:39:35+00:00",
      "offline_grace_until": "2026-10-24T10:39:35+00:00"
    },
    "signature": "wYwFAPoyaaIVtq8zvUHlQeyqV91sI7115WJHUzY7unDEjOts7MrN_OL3PZAd-Ahd-OdfI-EjIwAVwhfLbYQlDQ",
    "algorithm": "Ed25519",
    "key_id": "ed25519_2b9bc7e214b1209f"
  },
  "request_id": "req_b8b3d83f5a1820e7c2f16167"
}

Response fields

FieldTypeRequiredConstraintsDescriptionExample
successbooleanyesconst=trueThe operation succeeded.true
validbooleanyesconst=trueServer accepted this validation; still verify the signed snapshot.true
licenseobjectyesSee descriptionOnly payload is signed. This is PHP-specific canonical JSON, not RFC8785.Nested object/array shown in full JSON
license.payloadobjectyesSee descriptionExact signed snapshot. Signature authenticity alone does not grant access.Nested object/array shown in full JSON
license.payload.license_idstringyesSee descriptionLicense public identifier."lic_578581bd80c2a672da30fc29"
license.payload.product_idstringyesSee descriptionPublic App ID; compare to configured product."app_a642cd24641ec976626b"
license.payload.productstringyesSee descriptionProduct display name."Docs fixture product"
license.payload.planstringyesSee descriptionProduct-plan slug."fixture-plan"
license.payload.statusstringyesenum=["active", "suspended", "revoked", "expired", "pending", "cancelled"]Stored license status in lowercase."active"
license.payload.environmentstringyesenum="sandbox", "[live"]Bound license environment."sandbox"
license.payload.expires_atstring or nullyesformat="date-time"Independent license expiry; null has no configured expiry."2026-11-16T10:39:35+00:00"
license.payload.license_key_maskstringyesSee descriptionDisplay-only masked key."TESTDOCS-••••-••••-HAHH"
license.payload.starts_atstring or nullyesformat="date-time"Configured license start, currently not enforced by server eligibility."2026-10-17T10:39:35+00:00"
license.payload.created_atstring or nullyesformat="date-time"Creation timestamp; database-timezone discrepancy is disclosed."2026-10-17T12:39:35+00:00"
license.payload.max_devicesintegeryesSee descriptionConcurrent device limit.2
license.payload.max_activationsinteger or nullyesSee descriptionCumulative activation cap; null means no configured cap.4
license.payload.activation_countintegeryesSee descriptionCumulative consumed activations.1
license.payload.min_versionstring or nullyesSee descriptionOptional lower version restriction.null
license.payload.max_versionstring or nullyesSee descriptionOptional upper version restriction.null
license.payload.active_devicesintegeryesSee descriptionActive count at snapshot time.1
license.payload.customerobjectyesSee descriptionCustomer information; personal data, not an authentication credential.Nested object/array shown in full JSON
license.payload.customer.idstringyesSee descriptionCustomer public identifier."cus_d7db5516f8377f89a869e293"
license.payload.customer.namestring or nullyesSee descriptionOptional customer name."Synthetic contract customer"
license.payload.customer.emailstringyesSee descriptionCustomer email associated with this license."contract-6b9f0843f1d3609f@example.invalid"
license.payload.customer.companystring or nullyesSee descriptionOptional company.null
license.payload.customer.countrystring or nullyesSee descriptionOptional country.null
license.payload.product_detailsobjectyesSee descriptionProduct display metadata.Nested object/array shown in full JSON
license.payload.product_details.idstringyesSee descriptionPublic App ID."app_a642cd24641ec976626b"
license.payload.product_details.namestringyesSee descriptionProduct display name."Docs fixture product"
license.payload.product_details.platformstringyesSee descriptionLowercase configured product platform."desktop"
license.payload.plan_detailsobjectyesSee descriptionProduct plan, not the developer platform subscription.Nested object/array shown in full JSON
license.payload.plan_details.idstringyesSee descriptionInternal product-plan identifier."plan_3bf54c099326639ee183661f"
license.payload.plan_details.namestringyesSee descriptionPlan display name."Fixture plan"
license.payload.plan_details.slugstringyesSee descriptionPlan slug."fixture-plan"
license.payload.plan_details.billing_typestringyesSee descriptionLowercase product-plan billing type."monthly"
license.payload.plan_details.pricenumberyesSee descriptionPHP float; preserve numeric tokens such as19.0 for signatures.19.5
license.payload.plan_details.currencystringyesSee descriptionConfigured product-plan currency."EUR"
license.payload.plan_details.duration_daysinteger or nullyesSee descriptionConfigured duration in days.30
license.payload.plan_details.trial_daysinteger or nullyesSee descriptionConfigured trial duration in days.null
license.payload.featuresobject or arrayyesSee descriptionDynamic plan feature JSON values; empty PHP feature array may be[]. Validate types before use.Nested object/array shown in full JSON
license.payload.deviceobjectyesSee descriptionInstallation binding and descriptions.Nested object/array shown in full JSON
license.payload.device.idstringyesSee descriptionDevice public identifier."dev_299a6753c013047b3ed013de"
license.payload.device.installation_hashstring or nullyesSee descriptionSHA256 of supplied installation ID; null if validation header was omitted."e119b615ebfb5e5a2da5ea9c2a64b2117621399ff61d16718d460e0bb919a988"
license.payload.device.namestring or nullyesSee descriptionOptional device label; activation can return an empty string."Synthetic local installation"
license.payload.device.platformstring or nullyesSee descriptionOptional platform description."test"
license.payload.device.app_versionstring or nullyesSee descriptionOptional application version."1.0.0"
license.payload.server_timestringyesformat="date-time"Signed snapshot time in UTC."2026-10-17T10:39:35+00:00"
license.payload.next_validation_atstringyesformat="date-time"server_time plus product interval hours."2026-10-18T10:39:35+00:00"
license.payload.offline_grace_untilstringyesformat="date-time"server_time plus grace hours, not added after the validation interval."2026-10-24T10:39:35+00:00"
license.signaturestringyespattern="^[A-Za-z0-9_-]{86}$"Unpadded base64url Ed25519 signature over canonical payload bytes."wYwFAPoyaaIVtq8zvUHlQeyqV91sI7115WJHUzY7unDEjOts7MrN_OL3PZAd-Ahd-OdfI-EjIwAVwhfLbYQlDQ"
license.algorithmstringyesconst="Ed25519"Fixed algorithm, not negotiated."Ed25519"
license.key_idstringyesSee descriptionMust equal independently trusted key ID."ed25519_2b9bc7e214b1209f"
request_idstringyespattern="^req_[a-f0-9]{24}$"Request correlation ID. Replay bodies can retain an earlier ID."req_b8b3d83f5a1820e7c2f16167"

Response headers

X-Request-ID identifies the current request. Successful limited calls include X-RateLimit-Remaining and X-RateLimit-Reset, the latter in Unix seconds.

Errors omit those rate headers and Retry-After. See the complete rate-limit reference.

Error responses

CodeHTTPCauseFixRetryable?
RATE_LIMITED429Bucket exhausted; wait for window reset; no licensing Retry-After headerAfter window; apply jitterAfter the 60-second window, with jitter
INVALID_TOKEN401Missing/non-act_/unknown/revoked/deactivated activation tokenReactivate eligible installationNo blind retry; apply the correction
INVALID_REQUEST422Input constraints or entitlement/resource/renewal domain rejectionCorrect input/plan; not blind retryNo blind retry; apply the correction
DEVICE_NOT_AUTHORIZED403Token is active but associated device is not ACTIVEReactivation if eligibleNo blind retry; apply the correction
DEVICE_MISMATCH403Provided installation ID does not match token-bound HMAC hashUse original installation; do not copy tokenNo blind retry; apply the correction
LICENSE_EXPIRED403Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTPRenew eligible licenseNo blind retry; apply the correction
LICENSE_REVOKED403License REVOKED and no elapsed deadline taking precedenceDo not retry; contact developerNo blind retry; apply the correction
LICENSE_SUSPENDED403License SUSPENDED and no elapsed deadline taking precedenceRestore then reactivateNo blind retry; apply the correction
LICENSE_CANCELLED403License CANCELLED and no elapsed deadline taking precedenceDo not retry; new eligible licenseNo blind retry; apply the correction
LICENSE_PENDING403License PENDING and no elapsed deadline taking precedenceWait for authorized activationNo blind retry; apply the correction
INTERNAL_ERROR500Uncaught exception at global boundarySafe retry only for idempotent operation; preserve request_idOnly safe reads after backoff; reconcile mutations

An unregistered path or wrong method is handled by the global router as NOT_FOUND (HTTP 404), not405. Every licensing error has success=false, error.code, error.message and request_id. Errors keeps SDK-local and operational namespaces separate.

Side effects

Updates activation.last_validated_at and device.last_seen_at. It does not create a new token, consume another lifetime activation, emit a dedicated validation audit/webhook or send email. Existing tokens are checked against device/license state. Current validation does not consistently recheck product status/deletion; see the known limitations. An elapsed expiry is rejected, but the attempted EXPIRED update rolls back with the rejection; cron may later persist that state.

Authenticated requests can consume the organization monthly API usage counter once tenant context is established, including some failures. This usage counter is currently warning-only, not a monthly API rejection gate. Developer-key authentication can update last_used_at before a later failure. No request body or Authorization value is saved by the API metadata logger.

Idempotency and concurrency

Idempotency-Key is ignored. Bounded validation retries are possible after a transport failure, but do not loop after an explicit rejection. Respect signed next_validation_at and the client's verified-cache policy. Omission of X-Installation-ID is transport-accepted and returns a null signed installation_hash; official clients expect a bound hash, so always send the header.

Rate limits

120 requests per60 seconds for SHA256(IP + activation token). Malformed/missing token or installation headers can reject before the bucket is reached.

Fixed windows begin with the first hit after expiry. Attempted calls consume every bucket they reach, even if later business validation fails.

Runnable examples

These are complete native HTTP transport programs, not full SDK clients or entitlement verifiers. JSON request bodies and credentials come from KDP_* environment variables. They print raw responses to a private terminal; do not send their stdout to application/CI logs. Use the P5 verification guides before granting access.

PHP

Save every file shown below. Install the exact runtime/dependencies from the complete example archive README. Set the endpoint request JSON in KDP_BODY_JSON and the documented KDP_* credential variables in your private process environment.

Run php request.php validate. Expected: full JSON, exit 0 for success, exit 1 for an API rejection; exit 2 for setup/transport failure. The cURL sample checks HTTP status; parse JSON before use. Never authorize a feature from transport output alone.

request.phpphp
<?php
declare(strict_types=1);
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: PHP8.1+, cURL extension. Run: php request.php public-key
// Expected: JSON, exit0 success/1 rejection/2 setup or transport failure.
// Credentials/body via KDP_* variables. Stdout may contain new credentials:
// use a private terminal only, not application/CI logs. Verify signed state separately.
$operations = json_decode(<<<'JSON'
{
  "public-key": {
    "method": "GET",
    "path": "/public-key",
    "auth": "public"
  },
  "create-license": {
    "method": "POST",
    "path": "/licenses",
    "auth": "developer"
  },
  "activate": {
    "method": "POST",
    "path": "/licenses/activate",
    "auth": "publicApp"
  },
  "validate": {
    "method": "POST",
    "path": "/licenses/validate",
    "auth": "activation"
  },
  "deactivate": {
    "method": "POST",
    "path": "/licenses/deactivate",
    "auth": "activation"
  },
  "get-license": {
    "method": "GET",
    "path": "/licenses/{license}",
    "auth": "developer"
  },
  "revoke": {
    "method": "POST",
    "path": "/licenses/{license}/revoke",
    "auth": "developer"
  },
  "suspend": {
    "method": "POST",
    "path": "/licenses/{license}/suspend",
    "auth": "developer"
  },
  "restore": {
    "method": "POST",
    "path": "/licenses/{license}/restore",
    "auth": "developer"
  },
  "renew": {
    "method": "POST",
    "path": "/licenses/{license}/renew",
    "auth": "developer"
  },
  "reset-devices": {
    "method": "POST",
    "path": "/licenses/{license}/reset-devices",
    "auth": "developer"
  }
}
JSON, true, 512, JSON_THROW_ON_ERROR);
function required(string $name): string {
    $value = (string) getenv($name);
    if ($value === '' || str_contains($value, "\r") || str_contains($value, "\n")) throw new RuntimeException('Missing configuration.');
    return $value;
}
try {
    $name = $argv[1] ?? '';
    if ($argc !== 2 || !isset($operations[$name])) throw new RuntimeException('Invalid operation.');
    $operation = $operations[$name];
    $base = rtrim(getenv('KDP_BASE_URL') ?: 'https://licensing.kdpika.com/api/v1', '/');
    $url = parse_url($base);
    $loopback = ($url['scheme'] ?? '') === 'http' && in_array($url['host'] ?? '', ['127.0.0.1', 'localhost'], true) && getenv('KDP_ALLOW_LOOPBACK') === '1';
    if (!$url || empty($url['host']) || (($url['scheme'] ?? '') !== 'https' && !$loopback) || isset($url['user'], $url['pass']) || isset($url['user']) || isset($url['query']) || isset($url['fragment']) || ($url['path'] ?? '') !== '/api/v1') throw new RuntimeException('Invalid base URL.');
    $path = $operation['path'];
    if (str_contains($path, '{license}')) {
        $id = required('KDP_LICENSE_ID');
        if (!preg_match('/^[A-Za-z0-9_-]+$/D', $id)) throw new RuntimeException('Invalid sample license ID.');
        $path = str_replace('{license}', $id, $path);
    }
    $headers = ['Accept: application/json', 'User-Agent: KDPikaDocsExample/1.0 PHP'];
    if ($operation['auth'] === 'developer') {
        $key = required('KDP_API_KEY');
        if (!str_starts_with($key, 'lmp_sandbox_')) throw new RuntimeException('Sandbox key required.');
        $headers[] = 'Authorization: Bearer ' . $key;
    } elseif ($operation['auth'] === 'activation') $headers[] = 'Authorization: Bearer ' . required('KDP_ACTIVATION_TOKEN');
    elseif ($operation['auth'] === 'publicApp') {
        $headers[] = 'X-App-ID: ' . required('KDP_APP_ID'); $headers[] = 'X-Environment: SANDBOX';
    }
    if ($name === 'create-license') $headers[] = 'Idempotency-Key: ' . required('KDP_IDEMPOTENCY_KEY');
    if ($name === 'validate') $headers[] = 'X-Installation-ID: ' . required('KDP_INSTALLATION_ID');
    $curl = curl_init($base . $path);
    $options = [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_CUSTOMREQUEST => $operation['method']];
    if ($operation['method'] === 'POST') {
        $body = getenv('KDP_BODY_JSON') ?: '{}';
        if (!is_object(json_decode($body, false, 512, JSON_THROW_ON_ERROR))) throw new RuntimeException('JSON object required.');
        $headers[] = 'Content-Type: application/json'; $options[CURLOPT_POSTFIELDS] = $body;
    }
    $options[CURLOPT_HTTPHEADER] = $headers;
    curl_setopt_array($curl, $options);
    $raw = curl_exec($curl);
    if ($raw === false) throw new RuntimeException('Transport unavailable.');
    $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    // No curl_close: PHP objects release the handle; PHP8.5 deprecates that call.
    $result = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
    echo $raw . "\n";
    if ($status < 200 || $status >= 300 || ($result['success'] ?? null) !== true) {
        fwrite(STDERR, "The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying.\n"); exit(1);
    }
    exit(0);
} catch (Throwable) {
    fwrite(STDERR, "Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n"); exit(2);
}

Expected result

A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and request IDs; a successful transport or unsigned management response does not grant a feature.

Common mistakes

  • Mixing Public App IDs, internal product IDs and product-plan IDs.
  • Assuming a mutation timeout means no work committed, or assuming Idempotency-Key protects every action.
  • Logging raw responses that contain a newly issued license key or activation token.

Security notes

Keep lmp_ credentials out of shipped desktop/browser code. Verify signed payload bytes and product/environment/installation/status/time/feature policy. The examples do not fix the documented idempotency, cache canonicalization or current-key-only rotation limitations.

Troubleshooting

SymptomCauseAction
RATE_LIMITED (HTTP 429)Bucket exhausted; wait for window reset; no licensing Retry-After headerAfter window; apply jitter
INVALID_TOKEN (HTTP 401)Missing/non-act_/unknown/revoked/deactivated activation tokenReactivate eligible installation
INVALID_REQUEST (HTTP 422)Input constraints or entitlement/resource/renewal domain rejectionCorrect input/plan; not blind retry
DEVICE_NOT_AUTHORIZED (HTTP 403)Token is active but associated device is not ACTIVEReactivation if eligible
DEVICE_MISMATCH (HTTP 403)Provided installation ID does not match token-bound HMAC hashUse original installation; do not copy token
LICENSE_EXPIRED (HTTP 403)Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTPRenew eligible license
LICENSE_REVOKED (HTTP 403)License REVOKED and no elapsed deadline taking precedenceDo not retry; contact developer
LICENSE_SUSPENDED (HTTP 403)License SUSPENDED and no elapsed deadline taking precedenceRestore then reactivate
LICENSE_CANCELLED (HTTP 403)License CANCELLED and no elapsed deadline taking precedenceDo not retry; new eligible license
LICENSE_PENDING (HTTP 403)License PENDING and no elapsed deadline taking precedenceWait for authorized activation
INTERNAL_ERROR (HTTP 500)Uncaught exception at global boundarySafe retry only for idempotent operation; preserve request_id

Next steps

Read idempotency and signed responses before integrating a purchase-to-license workflow.

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