# KDPika Licensing documentation
Published pages only. API v1; SDK versions are separate.
# Add licensing to your software
Canonical: https://licensing.kdpika.com/docs
---
slug: "/docs"
title: "Add licensing to your software"
description: "A clear path from first Sandbox activation to a safely assessed integration, with instant access to expert references."
section: "Home"
order: 1
audience: "all"
level: "beginner"
last_updated: "2026-10-21"
version: "v1"
related: []
draft: false
anchor_aliases: ["what-you-will-learn","prerequisites","start-here","popular-pages","api-reference","sdks","guides","changelog","support","expected-result","common-mistakes","security-notes","troubleshooting","next-steps","related"]
layout: "gateway"
---
## Start here
[Start the step-by-step guide](/docs/v1/integration-guide).
## Guides
Follow the [14-stage integration roadmap](/docs/v1/integration-guide), [choose your app type](/docs/v1/start/choose-your-path), or [try your first license](/docs/v1/start/quickstart).
## API reference
[Find an API endpoint](/docs/v1/api).
## SDKs
[Official SDKs and REST clients](/docs/v1/sdks/overview). Python 1.1.5 is published on [PyPI](https://pypi.org/project/kdpika-licensing/) with MIT-licensed [GitHub source](https://github.com/kdpika/kdpika-licensing).
## Changelog
[Documentation and API changes](/docs/v1/changelog).
## Popular pages
[Activation](/docs/v1/api/activate), [errors](/docs/v1/api/errors), [security](/docs/v1/security), [current limitations](/docs/v1/integration-notes).
## Earlier overview notes
Every earlier overview explanation is preserved in [overview reference notes](/docs/v1/help/previous-home).
# Using the documentation
Canonical: https://licensing.kdpika.com/docs/v1/documentation
---
title: "Using the documentation"
description: "Read, search, navigate and export KDPika Licensing documentation without an account."
slug: "/docs/v1/documentation"
section: "Documentation"
order: 1
audience: "all"
level: "beginner"
last_updated: "2026-10-15"
version: "v1"
related: ["/docs/v1/components", "/docs/v1/integration-notes"]
draft: false
---
Read published documentation without creating an account or enabling JavaScript.
## What you will learn
- Find a published page with search, navigation or a direct link.
- Choose a theme, programming-language tab and readable code display.
- Export a page and send privacy-conscious feedback.
## Prerequisites
- A browser with HTTPS support for the [public documentation](/docs).
- JavaScript is optional for reading; interactive search and copy controls need it.
## Navigation and versions
The sidebar groups published pages. Open a group with its disclosure control, then follow a page link. Breadcrumbs show your position. Previous and next links follow the published order, not an unfinished guide.
The API version indicator shows v1. SDK package versions are separate. A future API version can have its own navigation without changing existing v1 links.
## Search and keyboard shortcuts
Press Ctrl+K on Windows or Linux, or Command+K on macOS, to open search. Type a phrase, use Arrow Down or Arrow Up to move through section results, then press Enter to open a result. Escape closes the dialog and restores focus.
Search runs in your browser over a local index. Queries are not sent to an external search provider or stored as analytics. After the page and index have loaded online, cached search can work without a connection; a first-ever offline visit has no cache to use.
Without JavaScript, use sidebar links, browser Find and the page's On this page links.
## Code and language preferences
Language tabs keep your last choice across pages when browser local storage is available. All tab contents remain readable without JavaScript. The selected tab supports arrow keys, Home and End.
Copy copies the original code, not line numbers or syntax markup. Wrap toggles long lines. Highlighted lines draw attention without changing the text you copy.
The presentation examples below let you confirm that a language choice survives navigation between this page and the component page. They print a label only; neither calls the licensing API.
:::tabs
:::tab python Python
```python
"""Purpose: documentation component demo. Requires Python 3.9+, no dependencies.
Run: python3 docs-content/samples/presentation/python.py
Expected output: KDPika Licensing documentation
"""
print("KDPika Licensing documentation")
```
:::
:::tab javascript JavaScript
```javascript
// Purpose: documentation component demo. Requires Node.js 18+, no dependencies.
// Run: node docs-content/samples/presentation/javascript.js
// Expected output: KDPika Licensing documentation
console.log("KDPika Licensing documentation");
```
:::
:::
## Theme and print
The initial theme follows your system light or dark preference. Choose System, Light or Dark to override it. A blocked browser storage area does not stop reading. Print removes navigation and interactive controls and uses a high-contrast document layout.
Theme and language choices use local browser storage for these preferences only. The documentation cache stores public pages and the search index; it never caches account pages, feedback submissions or credentials. No documentation analytics or advertising identifier is added.
## Markdown and AI exports
Use Copy page as Markdown or the Markdown download link. Each published page has a matching .md URL. The root [AI index](/llms.txt) lists published pages; the [complete export](/llms-full.txt) contains their Markdown. Draft pages are absent from search, navigation, sitemap and exports.
## Feedback and support
Choose Yes or No below a page and optionally explain what was unclear. Do not include names, email addresses, [license keys](/docs/v1/start/glossary#license-key), payment details or private application information. Feedback stores the page, answer, comment and time for up to 30 days. A keyed network-address digest is used only for short-lived abuse limits, not attached to feedback records. Feedback is not a private support channel.
For account help, use [support email](mailto:hi@kdpika.com). Edit this page and Report an issue open a prepared email; no public repository is implied. [Privacy information](/legal/privacy) explains wider platform processing.
## Expected result
You can read a page while signed out, move to a heading, search a section, copy a code block and download Markdown. A successful feedback submission displays a confirmation without creating an account.
## Common mistakes
- Searching while offline before the index has been cached: reconnect once and open search.
- Treating the health-check link as an uptime history: it shows the existing check, not an SLA.
- Pasting a license key into feedback: remove private data and use the correct support channel.
## Security notes
Documentation never asks for a [developer API key](/docs/v1/start/glossary#developer-api-key). Search input is displayed as text. Feedback requires a same-origin request and a signed, expiring token; rate limits can reject repeated submissions. No client-only anti-spam mechanism guarantees the absence of automated submissions.
## Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| Search has no results | No published section matches the phrase | Try a shorter term or browse navigation |
| Copy is unavailable | Clipboard permissions or insecure local context | Select the code and copy with the keyboard |
| Feedback asks you to reload | Cached form token expired | Reload while online and submit again |
| Preferences are not remembered | Browser storage is unavailable or cleared | Choose again; reading still works |
## Next steps
Inspect the [documentation components](/docs/v1/components) and read the [current integration limitations](/docs/v1/integration-notes).
## Related
- [Documentation index](/docs)
- [Contact support](mailto:hi@kdpika.com)
# Documentation components
Canonical: https://licensing.kdpika.com/docs/v1/components
---
title: "Documentation components"
description: "Try the documentation's callouts, language tabs, code blocks, tables, diagrams and keyboard controls."
slug: "/docs/v1/components"
section: "Documentation"
order: 2
audience: "all"
level: "beginner"
last_updated: "2026-10-15"
version: "v1"
related: ["/docs/v1/documentation", "/docs/v1/integration-notes"]
draft: false
---
This page demonstrates the documentation interface, not a licensing integration or a complete API reference.
## What you will learn
- Recognize the five callout types and readable component styles.
- Test language tabs, copy buttons, line highlighting and wrapping.
- Use accessible tables, diagrams, steps and disclosure controls.
## Prerequisites
- Read [Using the documentation](/docs/v1/documentation).
- Python 3.9 or Node.js 18 is needed only to run the small presentation examples below.
## Callouts
:::note Note
Extra context supports the surrounding explanation.
:::
:::tip Tip
An optional improvement does not replace a required step.
:::
:::warning Warning
A warning identifies a risk that can cause a failed integration or lost work.
:::
:::danger Danger
Keep secret credentials out of distributed application code and public messages.
:::
:::security Security
Require a valid [license](/docs/v1/start/glossary#license) result before trusting its [feature](/docs/v1/start/glossary#feature) values. The detailed implementation notes are [public](/docs/v1/integration-notes).
:::
## Language tabs and runnable presentation examples
These complete programs print a documentation label. They do not activate a license, contact the API or demonstrate authorization. The files shown here are the files executed by the documentation checks.
:::tabs
:::tab python Python
```python
"""Purpose: documentation component demo. Requires Python 3.9+, no dependencies.
Run: python3 docs-content/samples/presentation/python.py
Expected output: KDPika Licensing documentation
"""
print("KDPika Licensing documentation")
```
:::
:::tab javascript JavaScript
```javascript
// Purpose: documentation component demo. Requires Node.js 18+, no dependencies.
// Run: node docs-content/samples/presentation/javascript.js
// Expected output: KDPika Licensing documentation
console.log("KDPika Licensing documentation");
```
:::
:::
Run python3 docs-content/samples/presentation/python.py or node docs-content/samples/presentation/javascript.js from the repository root. Expected output: [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) documentation.
## Highlighted code and wrapping
```bash filename="presentation.sh" highlight="2"
# Purpose: demonstrate a readable code block; requirement: POSIX shell.
printf '%s\n' 'KDPika Licensing documentation'
```
Run the two lines in a POSIX shell. Expected output: KDPika Licensing documentation. The highlighted line remains unchanged when copied.
## Numbered steps
:::steps
1. Choose a programming-language tab. Its panel becomes active.
2. Select Copy. A confirmation announces clipboard success or gives a manual-copy fallback.
3. Select Wrap. Long lines wrap without changing the program.
:::
## Badges and endpoint header
{{badge:official|Official SDK}}
{{badge:rest|REST client}}
{{badge:scope|licenses:read}}
{{badge:method|GET}}
These are presentation badges, not a claim that a new SDK or endpoint exists.
{{endpoint:GET /api/v1/public-key|Public verification key endpoint, shown only to demonstrate the header component.}}
The route exists in API v1. This component demonstration is not its complete endpoint guide.
## Field table
| Field | Type | Required | Explanation |
|---|---|---|---|
| title | string | Yes | Documentation-page title |
| draft | boolean | Yes | Hidden pages are excluded from public output |
| related | array | Yes | Links to published related pages |
This is the documentation metadata format, not a licensing request schema.
## Accessible diagram

Markdown source is validated, compiled and served as public documentation; drafts remain private.
## FAQ accordion
:::faq Can I read a page without JavaScript?
Yes. Page text, code, tables, links and disclosure content are rendered by the server. Interactive search and copy buttons enhance this readable baseline.
:::
:::faq Do the example programs send credentials?
No. These presentation programs only print a label.
:::
## Card grid
:::cards
- [Read the platform guide](/docs/v1/documentation)
- [Check integration limitations](/docs/v1/integration-notes)
:::
## Expected result
You can operate controls with Tab, Enter and Space. Tab lists also support arrow keys, Home and End. Every diagram has a text alternative and every table has a labelled scroll region.
## Common mistakes
- Treating a component demonstration as a licensing sample: it has no [activation](/docs/v1/start/glossary#activation) or authorization logic.
- Assuming a visual badge proves conformance: review the maintained SDK's actual limitations and tests.
## Security notes
Raw author HTML is escaped. Samples are included only from the approved samples directory. Diagrams are validated SVG assets, not runtime scripts. No credential is required to try these controls.
## Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
| Both language panels are visible | JavaScript is disabled | Read either complete panel; this is the no-JavaScript fallback |
| Diagram cannot load | Asset request failed | Read its adjacent text alternative and reload online |
## Next steps
Return to [Using the documentation](/docs/v1/documentation) for search and export controls.
## Related
- [Documentation index](/docs)
- [Integration limitations](/docs/v1/integration-notes)
# Introduction
Canonical: https://licensing.kdpika.com/docs/v1/start/introduction
---
order: 2
slug: "/docs/v1/start/introduction"
title: "Introduction"
section: "Start here"
description: "Understand what KDPika Licensing does, how it connects to your application, and where your own payment backend fits."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
[KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) is a managed licensing service for software [developers](/docs/v1/start/glossary#developer), not a checkout for their software buyers.
## What you will learn
- Where licensing fits into your application.
- Which responsibilities belong to you and to KDPika.
- Which starting path fits your role.
## Prerequisites
- An application you develop or maintain.
- A browser for reading these public guides. No account is required to read.
- The [glossary](/docs/v1/start/glossary) for unfamiliar terms.
## What the service does
Your [organization](/docs/v1/start/glossary#organization) owns [products](/docs/v1/start/glossary#product), [product plans](/docs/v1/start/glossary#product-plan), [customers](/docs/v1/start/glossary#customer) and [licenses](/docs/v1/start/glossary#license). Issue a license, bind an installation, validate its current eligibility and read its [features](/docs/v1/start/glossary#feature). The dashboard and management API support license operations, [device](/docs/v1/start/glossary#device) management and activity inspection.
Your [platform plan](/docs/v1/start/glossary#platform-plan) controls workspace capacity. A product plan defines licensing rules for your own application. These are separate commercial relationships.
## What the service does not do
KDPika does not collect payments from your software buyers. Stripe and Mollie in platform checkout bill your KDPika subscription. Your own store or payment backend confirms your software sales and decides when to issue, renew, suspend or revoke a customer license.
KDPika does not package your application, guarantee that a binary cannot be patched, supply a floating-seat lease server or provide an air-gapped activation-file workflow. Do not assume an advertised capability of a different licensing provider exists here.
## Architecture

Your app activates an installation, verifies the signed response locally and gates features. Your trusted server uses a developer API key for license management.
The client branch uses a [public App ID](/docs/v1/start/glossary#public-app-id) plus a customer [license key](/docs/v1/start/glossary#license-key). After [activation](/docs/v1/start/glossary#activation), it holds an [activation token](/docs/v1/start/glossary#activation-token). It never needs a secret [developer API key](/docs/v1/start/glossary#developer-api-key).
The management branch stays on your trusted server. For a sale, your backend first confirms payment with your own provider, then calls the license-management API. An untrusted success-page URL must not create an entitlement.
## Start by role
:::cards
- [Desktop developer: first signed activation](/docs/v1/start/quickstart)
- [Backend developer: protect server-side actions](/docs/v1/start/use-cases#saas-backend)
- [CLI developer: persist one installation identity](/docs/v1/start/use-cases#cli-or-developer-tool)
- [Plugin developer: keep management keys off customer hosts](/docs/v1/start/use-cases#plugin-or-extension)
:::
## Expected result
You can identify your application, your licensing backend and your separate sales backend without confusing a [platform subscription](/docs/v1/start/glossary#platform-subscription) with a software license.
## Common mistakes
- Shipping a developer API key in an installer: client activation does not require that key.
- Expecting a platform subscription payment to sell your application: your software-buyer checkout is separate.
- Calling a device ID a hardware fingerprint: it is a stable installation identifier supplied by your app.
## Security notes
Always require a valid result before reading entitlements. A signature protects integrity, not confidentiality. A user who controls the local machine may patch your application; protect valuable server resources on your backend.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| Unsure whether you need an SDK | The client and management paths are mixed | Use an SDK in the application; use scoped management credentials on your backend |
| Payment succeeded but no software key exists | Your sales backend has no issuance integration | Connect confirmed order fulfilment to the management API |
| Need floating or permanently offline licensing | That model is not built in | Ask support about requirements before buying |
## Next steps
[Choose your path](/docs/v1/start/choose-your-path), then run the [Sandbox quickstart](/docs/v1/start/quickstart).
## Related
- [Core concepts](/docs/v1/start/core-concepts)
- [Authentication](/docs/v1/start/authentication)
- [Current integration limitations](/docs/v1/integration-notes)
# Choose your path
Canonical: https://licensing.kdpika.com/docs/v1/start/choose-your-path
---
slug: "/docs/v1/start/choose-your-path"
title: "Choose your path"
description: "Select an application type and programming language, or choose the no-code license-issuance route."
section: "Start here"
order: 3
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
anchor_aliases: ["what-you-will-learn","prerequisites","choose-by-application","choose-by-language","choose-by-responsibility","expected-result","common-mistakes","security-notes","troubleshooting","next-steps","related"]
layout: "hub"
---
## What are you building?
Choose Desktop app, Web / SaaS backend, CLI tool, Plugin / extension, or manual issuance. Select your language at [Choose your path](/docs/v1/start/choose-your-path). The guide supports four official SDKs and ten REST client routes.
## Where access is enforced
Desktop and CLI tools check inside their own process. SaaS protected operations require your authenticated backend. JavaScript means Node/Electron main, not a browser SDK. Customer-controlled plugins cannot protect your online resources without server authorization.
## I just want to issue keys
Use [Stage 12: Deliver licenses](/docs/v1/integration-guide/deliver) for manual issuance and the store/no-code routes. Sending a key needs no SDK; the software receiving it still needs a correct licensing integration.
## Full architecture reference
The [previous path reference](/docs/v1/help/previous-choose-your-path) preserves every earlier application, language and responsibility note.
## Next steps
[Open the integration roadmap](/docs/v1/integration-guide)
# Current integration limitations
Canonical: https://licensing.kdpika.com/docs/v1/integration-notes
---
title: "Current integration limitations"
description: "Read the source-confirmed SDK, signature, storage and retry limitations before integrating KDPika Licensing."
slug: "/docs/v1/integration-notes"
section: "Integration notes"
order: 3
audience: "all"
level: "intermediate"
last_updated: "2026-10-21"
version: "v1"
related: ["/docs/v1/documentation", "/docs/v1/components"]
draft: false
---
These existing P2 integration notes describe unresolved behavior, not fixes or a production-readiness certificate.
## What you will learn
- Identify signature and feature-guard limitations in the maintained SDKs.
- Understand current token storage, runtime and key-rotation boundaries.
- Avoid assuming an ambiguous creation error means no [license](/docs/v1/start/glossary#license) was created.
## Prerequisites
- Use synthetic [Sandbox](/docs/v1/start/glossary#sandbox) data for integration testing.
- Keep [developer API keys](/docs/v1/start/glossary#developer-api-key) on trusted servers, not distributed client applications.
## Validity before features
Always require result.valid or Valid before reading any [feature](/docs/v1/start/glossary#feature). Legacy/source SDK feature helpers may return a stored value when the result is invalid. Python 1.1.5 helpers fail closed, but keep the explicit validity and signature gate in your application. No failed-verification or invalid result should unlock paid features.
## Published Python package
Python 1.1.5 is available on [PyPI](https://pypi.org/project/kdpika-licensing/) with MIT-licensed [GitHub source](https://github.com/kdpika/kdpika-licensing). Use Python 3.11–3.14, import kdpika_licensing and follow the [current Python guide](/docs/v1/sdks/python). Earlier Python number/Unicode canonicalization defects are corrected; this does not change the other SDKs.
## Signature and cache compatibility
JavaScript can reject signatures containing PHP integral-float values such as 19.0. C# can reject valid signatures after cached JSON re-encodes Unicode. Test actual server responses and cache round trips. Do not disable Ed25519 signature verification to work around a failure.
## Runtime and storage
PHP source requires PHP 8.2 syntax although the current Composer constraint says 8.1. Python can use an optional usable OS keyring. PHP, JavaScript and C# currently use permission-protected files, not built-in native credential stores. File permissions are not encryption or protection from the owner of a machine.
## Rotation and offline boundaries
Only the current signing key is published; no historical-key overlap is implemented. Pinned clients need coordinated updates. Offline access requires a previously verified response within its validity and grace deadlines. An authoritative server rejection does not permit an offline fallback. A user who controls a computer can patch a local application.
## Creation idempotency and retries
Creation replay records retain the returned [license key](/docs/v1/start/glossary#license-key) for 24 hours. The create and replay-storage operations are not atomic. A retry using an expired record can fail after another license has committed; concurrent requests can race. Investigate an ambiguous outcome with support before issuing another license. Do not assume exactly-once business fulfillment.
## Environments, allowances and time
Missing [activation](/docs/v1/start/glossary#activation) environment headers default to Sandbox. Developer API keys and [activation tokens](/docs/v1/start/glossary#activation-token) determine their own environment. [Products](/docs/v1/start/glossary#product) and [product plans](/docs/v1/start/glossary#product-plan) are shared configuration; usage allowances aggregate both environments.
Product and active-license creation limits are enforced. Monthly API usage is monitored with warnings, while platform [device](/docs/v1/start/glossary#device)/member figures are not separate activation gates. Per-license device limits are enforced. Support-level labels are not response-time promises.
PHP uses UTC, but the database connection does not normalize its session timezone. Non-UTC database hosts can produce inconsistent timestamp interpretation; review actual hosting configuration before relying on dates.
## Expected result
Your application rejects invalid license results and signature failures. Your release checklist includes real response and cache tests rather than relying only on a mocked SDK lifecycle test.
## Common mistakes
- Reading a true feature value without checking the license result first.
- Retrying an ambiguous creation blindly and issuing duplicate licenses.
- Treating an optional keyring as guaranteed native secure storage on every computer.
## Security notes
Never send API keys, activation tokens or full license keys in documentation feedback. Report a suspected vulnerability privately through [support](mailto:hi@kdpika.com). These disclosures do not establish a certification or eliminate the need for independent review.
## Troubleshooting
| Symptom | Current possible cause | Safe action |
|---|---|---|
| Valid server signature rejected in JavaScript | Integral-float serialization differs | Reproduce with synthetic data; do not bypass verification |
| C# cache fails after a restart | Unicode serialization differs | Test a cache round trip and report the failure without secrets |
| Create returns an ambiguous error | License may already have committed | Inspect the operation before creating again |
| Cached expiry looks offset | Database session timezone is not normalized | Review host timezone and actual timestamps |
## Next steps
Read [Using the documentation](/docs/v1/documentation) and contact [support](mailto:hi@kdpika.com) about a release blocked by these limitations.
## Related
- [Public security overview](/#security)
- [Privacy information](/legal/privacy)
# Quickstart: your first Sandbox license
Canonical: https://licensing.kdpika.com/docs/v1/start/quickstart
---
slug: "/docs/v1/start/quickstart"
title: "Quickstart: your first Sandbox license"
description: "Create a verified workspace, issue a test license and run one complete signed activation example on a single page."
section: "Start here"
order: 4
audience: "all"
level: "beginner"
last_updated: "2026-10-21"
version: "v1"
related: []
draft: false
layout: "quickstart"
anchor_aliases: ["csharp-setup", "curl-setup", "what-you-will-learn","prerequisites","create-the-sandbox-records","verify-your-account","create-your-product","create-your-plan-and-feature","issue-a-test-license","download-your-sdk","configure-your-environment","choose-a-runtime","common-mistakes","security-notes","troubleshooting","related"]
---
## What you will build
A small [Sandbox](/docs/v1/start/glossary#sandbox) program that activates a key and checks the server's signed decision before reporting access. Plan for about ten minutes after email and runtime setup; installation and email [delivery](/docs/v1/start/glossary#delivery) can take longer.
## What you need
An inbox you control, an internet connection and one of the runtimes listed below. No account or licensing knowledge is assumed. Use a user-owned example folder and private state directory.
## 1. Create your Sandbox workspace
1. Open [registration](/register) and select Free for Sandbox evaluation.
2. Enter an email you control and verify the code or single-use link from the message.
3. Complete your name, workspace name, password and terms acknowledgement.
4. In the workspace header, select **Sandbox**.
A workspace is the private area that contains your [products](/docs/v1/start/glossary#product) and [licenses](/docs/v1/start/glossary#license). Sandbox means test records, separate from [Live](/docs/v1/start/glossary#live). A paid platform selection can require checkout; it is not a [customer](/docs/v1/start/glossary#customer) license purchase.
## 2. Create your product and plan
1. Open **Products → New product** (or **Create product** on an empty page).
2. Enter **Product name** Quickstart App, **Slug** quickstart-app, **Platform** Desktop, and **License prefix** QUICK. Choose **Create product**.
3. Saving opens the first-plan form in **Plans & [features](/docs/v1/start/glossary#feature)**. If it is closed, choose **New plan**.
4. Select Quickstart App. Enter **Plan name** Quickstart plan and **Slug** quickstart-plan.
5. Choose **Currency** EUR, **Price** 19.50, **Billing type** Monthly, **Duration days** 30, **Max [devices](/docs/v1/start/glossary#device)** 2 and **Feature keys** pdf_export.
6. Choose **Create plan**. Return to Products and copy the product's public **App ID**.
A product represents your software. Its App ID is the application identifier; there is no separate Application record to create. A plan describes access for that product. These plan prices are reference metadata, not a payment or recurring charge.
:::warning Use the demonstrated test values
The sample price and name are test data. Python SDK 1.1.5 fixes the earlier Python canonicalization defects; legacy JavaScript/C# limitations remain documented. Test your real payloads and never weaken verification. A production assessment is required at Stage 14.
:::
## 3. Issue your test key
1. Open **Licenses** while the header still says **Sandbox**.
2. Choose **New license** or **Create first license**.
3. Select Quickstart App / Quickstart plan in **Product and plan**.
4. Enter your test **Customer email** and a simple **Customer name**, such as Quickstart Tester. Company and External customer ID are optional.
5. Choose **Create license**. Copy **Your new [license key](/docs/v1/start/glossary#license-key)**, shown once, into your private test configuration.
The dashboard creates or reuses the customer for this email. A new license has no activated devices yet.
:::security Credential basics
The [public App ID](/docs/v1/start/glossary#public-app-id) identifies the software. A customer license key activates one purchase. The SDK receives a private [activation token](/docs/v1/start/glossary#activation-token) and stores an installation identity. A [developer API key](/docs/v1/start/glossary#developer-api-key) can manage licenses: create one under **API keys** only for a trusted sales backend, never for this client. If you supply an email during [activation](/docs/v1/start/glossary#activation), it must match the license.
:::
## 4. Install your chosen SDK
:::tabs
:::tab python Python
### Python setup
Use Python 3.11–3.14 and a working HTTPS certificate store. The official package is published on [PyPI](https://pypi.org/project/kdpika-licensing/) with [GitHub source](https://github.com/kdpika/kdpika-licensing). Create a virtual environment, then install:
```bash
python3 -m venv .venv
.venv/bin/python -m pip install "kdpika-licensing==1.1.5"
.venv/bin/python -m pip check
```
On native Windows use py -m venv .venv, then .venv\\Scripts\\python.exe for both pip commands and the example. Import kdpika_licensing. Use the same virtualenv interpreter for pip and your example.
:::
:::tab javascript JavaScript / Node.js
### JavaScript setup
Requires Node.js 18+ source compatibility, npm and an authenticated backend, CLI or Electron main process. Do not run this SDK in a web page. Download [JavaScript SDK 1.0.0](/docs/downloads/kdpika-javascript-sdk-1.0.0.zip) and extract into kdpika-javascript-sdk, then run:
```bash
npm install --ignore-scripts --save-exact ./kdpika-javascript-sdk
```
Save your example with the .mjs extension. The installed module is @kdpika/license-sdk. Choose a maintained Node runtime for production and run the compatibility tests there.
:::
:::tab php PHP
### PHP setup
Requires PHP 8.2+, cURL, Sodium and Composer. The package's older ^8.1 declaration is not its actual minimum. Download [PHP SDK 1.0.0](/docs/downloads/kdpika-php-sdk-1.0.0.zip) and extract into kdpika-php-sdk:
```bash
composer install --working-dir ./kdpika-php-sdk --no-interaction --no-plugins --no-scripts --prefer-dist
```
The example loads that folder's vendor/autoload.php. It runs on your trusted backend or CLI, not in browser JavaScript. KDP_SDK_DIR can point to a different extracted package folder.
:::
:::tab csharp C# / .NET
### C# setup
Requires the .NET SDK and the unchanged SDK's net7.0 source target. Download [C# SDK 1.0.0](/docs/downloads/kdpika-csharp-sdk-1.0.0.zip) and extract into kdpika-csharp-sdk. Save the project below beside that folder:
```xml
Exenet7.0enableenablefalse
```
```bash
dotnet restore Guide.csproj
```
The demonstrated net7 target is an existing compatibility constraint, not a recommendation to deploy an end-of-support runtime. Production needs a maintained-runtime compatibility assessment; see the disclosed SDK limitations.
:::
:::tab curl cURL / other language
### cURL setup
This is a transport exercise, not an SDK or signature verifier. Use this route for a REST-only language, then follow its real client and verification project before granting access.
```bash
umask 077
curl --fail --silent --show-error -o response-private.json \
-H "X-App-ID: $KDP_APP_ID" -H "X-Environment: SANDBOX" \
-H "Content-Type: application/json" \
--data '{"license_key":"YOUR_SANDBOX_LICENSE_KEY","device_id":"guide-install-CHANGE-ONCE"}' \
https://licensing.kdpika.com/api/v1/licenses/activate
```
Replace the key privately. Keep the device_id stable for this installation. The file contains a private activation token: do not publish or log it. HTTP success alone does not authorize your app. Read [activation](/docs/v1/api/activate), then choose your [REST client](/docs/v1/sdks/overview) and [signature verification](/docs/v1/security/verify-ed25519).
:::
:::
## 5. Configure your terminal
Set these variables in your terminal. App ID is public; your customer's key and state directory are private. Do not use a developer API key in this client.
:::tabs
:::tab bash macOS / Linux · Bash
```bash
export KDP_APP_ID="YOUR_PUBLIC_APP_ID"
export KDP_CUSTOMER_EMAIL="YOUR_TEST_CUSTOMER_EMAIL"
export KDP_STATE_DIR="$HOME/.kdpika-guide-sandbox"
read -r -s -p "Sandbox license key: " KDP_LICENSE_KEY
printf '\n'
export KDP_LICENSE_KEY
```
For a different device test, use a new private state directory, not a different email.
:::
:::tab powershell Windows · PowerShell
```powershell
$env:KDP_APP_ID = "YOUR_PUBLIC_APP_ID"
$env:KDP_CUSTOMER_EMAIL = "YOUR_TEST_CUSTOMER_EMAIL"
$env:KDP_STATE_DIR = Join-Path $env:LOCALAPPDATA "KDPikaGuideSandbox"
$secureKey = Read-Host "Sandbox license key" -AsSecureString
$env:KDP_LICENSE_KEY = [System.Net.NetworkCredential]::new("", $secureKey).Password
```
The variable is plaintext in this process; do not print it or leave it in a shared terminal. Close the terminal after testing. Windows Python uses .venv\\Scripts\\python.exe; Node/PHP/.NET commands are the same if their runtimes are installed.
:::
:::
Expected result: the variables are available only to this terminal and its child processes. Use a user-owned directory, outside a web root or shared repository. The sample uses Sandbox explicitly.
## 6. Save and run one complete file
:::tabs
:::tab python Python
Save **license-check.py** in your example folder.
```python
"""Sandbox learning example; use private state, never a developer API key."""
import os
import sys
from pathlib import Path
from kdpika_licensing import LicenseClient
try:
action = sys.argv[1] if len(sys.argv) == 2 else "activate"
if action not in ("activate", "validate", "check", "deactivate"):
raise ValueError("Choose an action")
client = LicenseClient(
product=os.environ["KDP_APP_ID"], environment="SANDBOX",
base_url=os.getenv("KDP_BASE_URL", "https://licensing.kdpika.com"),
storage_dir=Path(os.environ["KDP_STATE_DIR"]), prefer_keyring=False)
result = client.activate(os.environ["KDP_LICENSE_KEY"],
os.getenv("KDP_CUSTOMER_EMAIL") or None) if action == "activate" else getattr(client, action)()
released = action == "deactivate" and result.valid
allowed = action != "deactivate" and result.valid and result.signature_valid
print("Device released." if released else "Sandbox license verified." if allowed
else "License could not be confirmed. Check your key or contact support.")
sys.exit(0 if released or allowed else 1)
except Exception:
print("License check unavailable. Check configuration and connection.", file=sys.stderr)
sys.exit(2)
```
Run:
```bash
.venv/bin/python license-check.py activate
```
:::
:::tab javascript JavaScript / Node.js
Save **license-check.mjs** in your example folder.
```javascript
// Node.js / Electron main process, not browser JavaScript.
import { LicenseClient } from '@kdpika/license-sdk';
try {
const action = process.argv[2] || 'activate';
if (!['activate', 'validate', 'check', 'deactivate'].includes(action)) throw Error();
const client = new LicenseClient({
product: process.env.KDP_APP_ID, environment: 'SANDBOX',
baseUrl: process.env.KDP_BASE_URL || 'https://licensing.kdpika.com',
storageDir: process.env.KDP_STATE_DIR
});
if (!process.env.KDP_STATE_DIR) throw Error();
const result = action === 'activate'
? await client.activate(process.env.KDP_LICENSE_KEY, process.env.KDP_CUSTOMER_EMAIL || null)
: await client[action]();
const released = action === 'deactivate' && result.valid;
const allowed = action !== 'deactivate' && result.valid && result.signatureValid;
console.log(released ? 'Device released.' : allowed ? 'Sandbox license verified.'
: 'License could not be confirmed. Check your key or contact support.');
process.exitCode = released || allowed ? 0 : 1;
} catch {
console.error('License check unavailable. Check configuration and connection.');
process.exitCode = 2;
}
```
Run:
```bash
node license-check.mjs activate
```
:::
:::tab php PHP
Save **license-check.php** in your example folder.
```php
activate(getenv('KDP_LICENSE_KEY') ?: '', getenv('KDP_CUSTOMER_EMAIL') ?: null)
: $client->$action();
$released = $action === 'deactivate' && $result->valid;
$allowed = $action !== 'deactivate' && $result->valid && $result->signatureValid;
echo $released ? 'Device released.' : ($allowed ? 'Sandbox license verified.'
: 'License could not be confirmed. Check your key or contact support.'), PHP_EOL;
exit($released || $allowed ? 0 : 1);
} catch (Throwable) {
fwrite(STDERR, 'License check unavailable. Check configuration and connection.'.PHP_EOL);
exit(2);
}
```
Run:
```bash
php license-check.php activate
```
:::
:::tab csharp C# / .NET
Save **Program.cs** in your example folder.
```csharp
using KDPika.LicenseSdk;
static string Required(string name) => Environment.GetEnvironmentVariable(name)
is string value && value.Length > 0 ? value : throw new ArgumentException();
try {
var action = args.Length == 0 ? "activate" : args[0];
if (!new[]{"activate","validate","check","deactivate"}.Contains(action)) throw new ArgumentException();
var client = new LicenseClient(Required("KDP_APP_ID"), "SANDBOX",
Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com",
storageDir:Required("KDP_STATE_DIR"));
var result = action switch {
"activate" => await client.ActivateAsync(Required("KDP_LICENSE_KEY"),
Environment.GetEnvironmentVariable("KDP_CUSTOMER_EMAIL")),
"validate" => await client.ValidateAsync(), "check" => await client.CheckAsync(),
_ => await client.DeactivateAsync()
};
var released = action == "deactivate" && result.Valid;
var allowed = action != "deactivate" && result.Valid && result.SignatureValid;
Console.WriteLine(released ? "Device released." : allowed ? "Sandbox license verified."
: "License could not be confirmed. Check your key or contact support.");
Environment.ExitCode = released || allowed ? 0 : 1;
} catch {
Console.Error.WriteLine("License check unavailable. Check configuration and connection.");
Environment.ExitCode = 2;
}
```
Run:
```bash
dotnet run --project Guide.csproj -- activate
```
:::
:::
## Expected result
The chosen [official SDK](/docs/v1/start/glossary#official-sdk) sample prints exactly:
```text
Sandbox license verified.
```
It exits with code 0 and the dashboard's Devices count shows one active installation. No real charge is made. A cURL-only success is a transport result, not a verified access grant.
## If it does not work
| What you see | What to do |
|---|---|
| INVALID_LICENSE | Check the full key, public App ID, Sandbox selection and matching customer email. |
| MAX_DEVICES_REACHED | Deactivate an old installation or inspect Devices. Do not make random identities. |
| SIGNATURE_INVALID / cached signature error | Keep access locked. Check the runtime, clock and SDK compatibility notes; do not disable verification. |
| NOT_ACTIVATED | Run activate in the same private state directory first. |
| NETWORK_ERROR / no connection | Reconnect. A first activation needs internet; no cached response means no offline grant. |
| LICENSE_EXPIRED / LICENSE_REVOKED / LICENSE_SUSPENDED | Explain the state to the customer and provide support; never silently convert it to valid. |
## Next steps
- [Continue with the Complete Integration Guide](/docs/v1/integration-guide).
- [Validate on every launch](/docs/v1/integration-guide/validate).
- [Review the launch gate](/docs/v1/integration-guide/go-live).
## Previous detailed reference
The [earlier quickstart reference](/docs/v1/help/previous-quickstart) retains all earlier explanations and example files. It is supplementary, not required for this page.
# Core concepts
Canonical: https://licensing.kdpika.com/docs/v1/start/core-concepts
---
order: 5
slug: "/docs/v1/start/core-concepts"
title: "Core concepts"
section: "Start here"
description: "Learn the relationships between your workspace, products, plans, customers, licenses, installations, tokens and events."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
A [product plan](/docs/v1/start/glossary#product-plan) describes access rules; a [license](/docs/v1/start/glossary#license) grants those rules to one [customer](/docs/v1/start/glossary#customer) in one environment.
## What you will learn
- How the main records relate.
- Which identifiers are public and which are secrets.
- Which limits and environments belong to which layer.
## Prerequisites
- Read the [introduction](/docs/v1/start/introduction).
- Use the [glossary](/docs/v1/start/glossary) when comparing similar names.
## Relationships

An organization owns products and customers. A product has product plans and features. A customer license references one plan and environment; devices and activation tokens belong to that license.
## Record reference
| Concept | Purpose | Important distinction |
| --- | --- | --- |
| [Organization](/docs/v1/start/glossary#organization) | Tenant owning the workspace records and [platform subscription](/docs/v1/start/glossary#platform-subscription) | Not a software buyer account |
| [Product](/docs/v1/start/glossary#product) | Your application definition, [public App ID](/docs/v1/start/glossary#public-app-id) and validation policy | Shared configuration across [Sandbox](/docs/v1/start/glossary#sandbox) and [Live](/docs/v1/start/glossary#live) |
| Public App ID | Nonsecret product identifier supplied as X-App-ID during [activation](/docs/v1/start/glossary#activation) | Not a secret [developer API key](/docs/v1/start/glossary#developer-api-key) |
| Product plan | Template for duration, [features](/docs/v1/start/glossary#feature), [device](/docs/v1/start/glossary#device) and cumulative activation limits | Its price is metadata, not a buyer checkout |
| Feature or entitlement | A named boolean or typed JSON value returned in signed data | Your app must enforce it after validation |
| Customer | Your software buyer or license recipient | Distinct from you paying KDPika |
| License | Customer/product/plan/environment access record with status and optional expiry | Not the printable [license key](/docs/v1/start/glossary#license-key) alone |
| License key | Secret used to activate a license | Copy it at issuance; listings show a mask |
| [Installation ID](/docs/v1/start/glossary#installation-id) | Stable random identity generated and persisted by your app | Not a MAC address or serial number |
| Device | Server record for a hashed installation identity | Not guaranteed unique physical hardware |
| Activation | Registering an installation and obtaining a token | Active seats and cumulative activations differ |
| [Activation token](/docs/v1/start/glossary#activation-token) | Secret used to validate or deactivate the activated installation | Bound to its license/device; do not share |
| Environment | Sandbox or Live boundary for licenses, customers, credentials and endpoints | Not the payment provider's Test/Live mode |
| Developer API key | Scoped server-side credential for management | Never distribute in application binaries |
| [Webhook](/docs/v1/start/glossary#webhook) | Signed outbound notification to your backend | Not an authority that decides license validity |
## Product and platform plans
Your platform subscription defines workspace capacity and billing. Your own product plans define what your customers may use. A monthly product plan does not automatically charge a buyer or renew a license after a sale.
Products, plans and features are shared configuration. Licenses and customers belong to an environment; devices inherit that environment through the license. Usage totals can span both licensing environments.
## Features and changes
Require a valid result before reading a feature. Current helpers can expose a payload value even when the result is invalid; the caller must apply the validity guard.
Device and cumulative activation limits are copied when issuing a license. Changing the product plan does not retroactively replace those copies. Feature resolution uses plan features with license-level overrides; an offline app cannot see a fresh change before reconnecting.
## Expected result
You can trace a customer license to its product, plan, environment, devices and token without confusing it with a platform billing subscription.
## Common mistakes
- Counting active licenses as activation requests: a license can have multiple devices and many validations.
- Resetting devices to clear cumulative activations: reset releases seats and revokes tokens, not that counter.
- Treating a feature as a usage meter: implement metering and business rules in your own backend.
## Security notes
Customer emails and metadata can be personal data. Hashing an installation ID does not automatically make the complete record anonymous. Avoid unnecessary personal data in labels and metadata.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| Existing limit did not change after editing a plan | The license stores its issued limits | Review the individual license and supported management operations |
| More than one device uses one license | Its per-license device policy allows it | Review max_devices and your transfer policy |
| A plan's price appears in a response but no charge occurs | Licensing metadata is not payment processing | Use your own confirmed payment/fulfilment flow |
## Next steps
Read [authentication](/docs/v1/start/authentication) and the [license lifecycle](/docs/v1/start/license-lifecycle).
## Related
- [Glossary](/docs/v1/start/glossary)
- [Environments](/docs/v1/start/environments)
- [Activation flow](/docs/v1/start/activation-validation-deactivation)
# Glossary
Canonical: https://licensing.kdpika.com/docs/v1/start/glossary
---
order: 6
slug: "/docs/v1/start/glossary"
title: "Glossary"
section: "Start here"
description: "Alphabetical definitions of every canonical KDPika Licensing term, from activation and credentials to products, signed state and webhooks."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/core-concepts","/docs/v1/start/authentication"]
draft: false
---
Find the precise meaning of every canonical term used in KDPika Licensing documentation.
## What you will learn
- Distinguish identifiers, secrets, records and platform billing terms.
- Read the same vocabulary across guides and examples.
## Prerequisites
- No account required.
- Read the [core relationships](/docs/v1/start/core-concepts) for context.
## Alphabetical terms
### Activation
Authorization of one installation and issuance of a token.
Usage: Activation; seat count and lifetime count differ.
### Activation count
Lifetime increments for new or inactive installation activation.
Usage: Activation count; device reset does not clear it.
### Activation token
act_ secret whose hashed row binds license/device.
Usage: Activation token; never Public App ID.
### Active devices
Number of ACTIVE device rows for a license.
Usage: Active devices; does not drop on suspension alone.
### Customer
Developer's software buyer/license recipient.
Usage: Customer; platform customer must be explicitly qualified.
### Delivery
Per-endpoint queued attempt series whd_ with at most6attempts.
Usage: Delivery; not exactly-once business execution.
### Demo data
Fictional dashboard/marketing example, not measured platform usage.
Usage: Demo data; visible adjacent to the mock, with an accessible group label.
### Developer
Software developer subscribing to KDPika Licensing.
Usage: Developer; not the developer's software customer.
### Developer API key
Product/environment-scoped lmp_ secret for trusted server management.
Usage: Developer API key; never embed in distributed client.
### Device
Server record binding a license to a hashed installation identity.
Usage: Device; not guaranteed physical hardware uniqueness.
### Feature
Named entitlement definition with boolean or typed JSON value.
Usage: Feature / entitlement; feature value needs valid license guard.
### Idempotency key
Organization-scoped create-request replay identity retained24h.
Usage: Idempotency key; not a universal all-endpoints guarantee.
### Installation ID
Persistent random identifier provided as device_id and X-Installation-ID.
Usage: Installation ID; no hardware fingerprint requirement.
### KDPika Licensing
Canonical product brand; a managed SaaS for software developers.
Usage: Preserve code/package identifiers; KDPika is the company name and constrained short display.
### License
Product/customer/environment entitlement record with lifecycle and limits.
Usage: License noun; never a synonym for API key.
### License key
Secret presented to activation; hashed/masked on license row but retained in idempotency response24h.
Usage: License key; not activation token.
### Live
Production license/customer/key/webhook environment, paid entitlement required.
Usage: Live display; LIVE header; live payload; Stripe mode is separate TEST/LIVE.
### Official SDK
Maintained Python/PHP/JavaScript/C# client, with currently logged conformance gaps.
Usage: Official SDK; never imply flawless/fullOS verification.
### Offline grace
Absolute signed offline_grace_until boundary used with expiry and validity checks.
Usage: Offline grace; not permission to ignore server rejection.
### Organization
Tenant/workspace owning products, customers, licenses and developer billing.
Usage: Organization; Workspace is a UI label; not a buyer account.
### Platform Admin
Operator with separately provisioned platform-admin privilege.
Usage: Platform Admin; workspace Owner is not Platform Admin.
### Platform plan
Free/Starter/Pro/Business capacity and billing tier for developer organization.
Usage: Platform plan; EUR source is runtime platform_plans.
### Platform subscription
Developer capacity/payment relationship with KDPika via Stripe or Mollie.
Usage: Platform subscription; not software-buyer payment processing.
### Product
Shared definition of a developer's application, public App ID and policy.
Usage: Product; not Stripe Product or platform subscription plan.
### Product plan
Developer-defined license template with price/duration/device/feature policy.
Usage: Product plan; not platform plan.
### Public App ID
Nonsecret app_ identifier used for activation via X-App-ID.
Usage: Public App ID; reject secret App key wording.
### Public verification key
Raw32byte Ed25519 key represented unpadded base64url.
Usage: Public key; not webhook signing secret.
### Request ID
req_ support correlation ID; replay body may retain original while header is current.
Usage: Request ID; no secret.
### REST client example
Transport example using the same API, not a full SDK.
Usage: REST client; not official SDK for remaining10languages.
### Sandbox
Separate test license/customer/key/webhook environment.
Usage: Sandbox display; SANDBOX header; sandbox payload.
### Scope
Registered action permission: licenses:read, licenses:write, devices:write.
Usage: Scope; arbitrary string does not grant a new API.
### Signed envelope
payload,signature,algorithm,key_id object using Ed25519.
Usage: Signed envelope; outer HTTP success/token/request_id is unsigned.
### Source-confirmed
Behavior inferred by reading exact source, not executed proof.
Usage: Distinguish source-confirmed, locally reproduced and unverified hosting/provider facts.
### Validation interval
Product hours until next normal online validation, measured from server_time.
Usage: Validation interval; grace is not added after it.
### Webhook
Optional signed outbound event to developer backend.
Usage: Webhook; licensing validity never waits for recipient.
### Webhook event ID
evt_ logical ID stable across retries/eligible recipient deliveries.
Usage: Event ID; not delivery ID.
### Workspace role
Organization membership OWNER/ADMIN/DEVELOPER/SUPPORT/VIEWER; actual route permissions must be checked.
Usage: Role is not a paid plan or subscription badge.
## Expected result
You can look up every term in the canonical terminology source and distinguish a customer license from your platform subscription.
## Common mistakes
- Calling every credential a license key.
- Confusing a workspace role with a subscription plan.
- Assuming a hashed identifier is inherently anonymous.
## Security notes
Examples of prefixes identify credential types; they are not usable secrets. Keep full activation tokens, license keys and developer API keys out of logs and feedback.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| Two names seem interchangeable | They describe different layers | Compare the definitions and core relationship diagram |
| A term differs from a JSON member | Prose naming and code identifiers differ | Preserve the exact API or SDK member in code |
| A scope name appears unfamiliar | Arbitrary scopes do not grant new endpoints | Use the registered scopes in authentication |
## Next steps
Read [authentication](/docs/v1/start/authentication) or return to [core concepts](/docs/v1/start/core-concepts).
## Related
- [Introduction](/docs/v1/start/introduction)
- [Activation flow](/docs/v1/start/activation-validation-deactivation)
- [Documentation home](/docs)
# Environments
Canonical: https://licensing.kdpika.com/docs/v1/start/environments
---
order: 7
slug: "/docs/v1/start/environments"
title: "Environments"
section: "Start here"
description: "Keep Sandbox and Live credentials, customer licenses and webhook traffic separate without confusing payment-provider mode."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
[Sandbox](/docs/v1/start/glossary#sandbox) tests your integration; [Live](/docs/v1/start/glossary#live) serves your real software [customers](/docs/v1/start/glossary#customer).
## What you will learn
- Which records are environment-specific and which configuration is shared.
- How the API chooses an environment.
- How platform allowances and payment modes differ from licensing environments.
## Prerequisites
- Read [core concepts](/docs/v1/start/core-concepts).
- Identify your [product](/docs/v1/start/glossary#product)'s [public App ID](/docs/v1/start/glossary#public-app-id) and the relevant credential type.
## Isolation rules
| Record or operation | Behaviour |
| --- | --- |
| Product, [product plan](/docs/v1/start/glossary#product-plan), [feature](/docs/v1/start/glossary#feature) definitions | Shared within your [organization](/docs/v1/start/glossary#organization) |
| Customer and [license](/docs/v1/start/glossary#license) | Stored in Sandbox or Live |
| [Device](/docs/v1/start/glossary#device) and [activation token](/docs/v1/start/glossary#activation-token) | Inherit the associated license's environment |
| [Developer API key](/docs/v1/start/glossary#developer-api-key) | Bound to one product, organization and environment |
| Outgoing [webhook](/docs/v1/start/glossary#webhook) endpoint | Has its own environment selection |
| Workspace usage | Counted across the organization, not a fresh allowance per environment |
## Request selection
[Activation](/docs/v1/start/glossary#activation) reads X-Environment, trims and uppercases it, accepts SANDBOX or LIVE, and defaults a missing header to SANDBOX. Send it explicitly to avoid relying on a default.
[Developer](/docs/v1/start/glossary#developer) management endpoints derive the environment from the authenticated developer API key. X-Environment does not override the key.
Validation and deactivation derive the environment from the activation token's license. They do not select a different license from an X-Environment header.
## Dashboard switch
The dashboard environment switch changes the current viewing/creation context. It does not convert an existing license or customer. Products and plans remain shared.
Free is Sandbox evaluation only. Live key, webhook and license creation needs the appropriate paid entitlement and eligible subscription. Existing customer-license validation is handled separately from creation limits.
## Payment modes are separate
Stripe or Mollie Test/Live mode controls [platform subscription](/docs/v1/start/glossary#platform-subscription) payments. It is not the licensing environment. A test subscription payment is not a real-money sale; changing payment mode does not migrate a Sandbox license.
## Safe testing
1. Select Sandbox before creating the test license.
2. Use synthetic customer data and a test mailbox you control.
3. Set SANDBOX explicitly in the client.
4. Keep Sandbox server keys separate from Live secrets.
5. Label test state directories so they cannot overwrite production client state.
Expected result: your tests create and modify only Sandbox records.
## Expected result
You know which environment each credential and record belongs to, and do not expect two independent monthly usage pools.
## Common mistakes
- Sending a Sandbox key with LIVE: it cannot activate that Live license.
- Sending X-Environment: LIVE beside a Sandbox developer key: the key remains Sandbox.
- Assuming an environment switch migrates data: issue the intended Live records deliberately.
## Security notes
Do not print either environment's secret keys in logs. Environment names are not a substitute for secret scoping and authorization. Do not run validation experiments against real customer credentials.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| INVALID_LICENSE 404 during activation | Key, supplied email, product or environment did not match | Check all four privately; the response deliberately does not disclose which identifier exists |
| LICENSE_NOT_FOUND 404 on management | The key's organization/product/environment does not match the resource | Use the correctly scoped key and license ID |
| Live option is unavailable | The workspace lacks eligible paid Live access | Review Billing; do not bypass the dashboard gate |
| Dashboard appears to have no licenses | You selected the other environment | Switch viewing context without modifying records |
## Next steps
Complete the [Sandbox quickstart](/docs/v1/start/quickstart), then the [go live checklist](/docs/v1/start/go-live-checklist).
## Related
- [Authentication](/docs/v1/start/authentication)
- [Core concepts](/docs/v1/start/core-concepts)
- [Current integration limitations](/docs/v1/integration-notes)
# Go live checklist
Canonical: https://licensing.kdpika.com/docs/v1/start/go-live-checklist
---
order: 8
slug: "/docs/v1/start/go-live-checklist"
title: "Go live checklist"
section: "Start here"
description: "Review credentials, signed decisions, failure handling, offline behaviour, webhook operations, monitoring and customer support before Live licensing."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
Move to [Live](/docs/v1/start/glossary#live) only after the actual application, its failure paths and its support process have been tested.
## What you will learn
- Review production readiness without confusing it with a successful demo.
- Plan safe credentials, [customer](/docs/v1/start/glossary#customer) support and operations.
- Identify known defects or missing operational evidence that block your own release.
## Prerequisites
- Complete the [Sandbox quickstart](/docs/v1/start/quickstart).
- Read [integration limitations](/docs/v1/integration-notes).
- Have eligible paid Live access and control of your application's release process.
## Credentials and environment
1. Confirm the paid platform entitlement and Live availability in Billing.
2. Issue Live customer [licenses](/docs/v1/start/glossary#license) deliberately; do not reuse Sandbox records.
3. Create separate minimum-scope Live server keys only where management is needed.
4. Put [public App ID](/docs/v1/start/glossary#public-app-id) and explicit LIVE selection in the app configuration.
5. Remove Sandbox sample keys, debug dumps and [developer API keys](/docs/v1/start/glossary#developer-api-key) from distributed artifacts.
6. Rehearse immediate key replacement and revocation without assuming an overlap window.
Detail: [environments](/docs/v1/start/environments) and [authentication](/docs/v1/start/authentication#create-a-key).
Expected result: each credential has an owner and a specific [product](/docs/v1/start/glossary#product), environment and consumer.
## Signed decisions and application enforcement
1. Require valid or Valid before every [feature](/docs/v1/start/glossary#feature) decision.
2. Test a real signed response for your product metadata, numeric features and customer text.
3. Test persisted cache round trips in the actual SDK release.
4. Verify product, environment and installation binding, expiry and signed deadlines.
5. Keep protected server actions authorized on your backend.
6. Record a signing-key rollout plan if you pin a [public verification key](/docs/v1/start/glossary#public-verification-key).
Detail: [activation flow](/docs/v1/start/activation-validation-deactivation) and [signature/cache limitations](/docs/v1/integration-notes).
Expected result: tampered, wrong-installation and invalid license results do not unlock access.
Known JavaScript integral-float and C# cache limitations remain release risks. Do not mark this checklist passed merely because the narrow demonstration data works.
## Failure paths and end-user UX
1. Test invalid key, wrong supplied email, wrong environment and installation mismatch.
2. Test [device](/docs/v1/start/glossary#device) exhaustion and cumulative activation exhaustion separately.
3. Test expired, suspended and revoked licenses.
4. Test timeout, unavailable server, missing cache, expired grace and clock rollback.
5. Test deactivation and a deliberate reinstall/transfer.
6. Use plain messages that say what the customer can do without inventing a payment cause.
Detail: [flow outcomes](/docs/v1/start/activation-validation-deactivation) and [lifecycle](/docs/v1/start/license-lifecycle).
Expected result: users can retry a network problem or contact the publisher; confirmed invalidation does not receive outage fallback.
## Offline policy and storage
1. Choose [validation interval](/docs/v1/start/glossary#validation-interval) and [offline grace](/docs/v1/start/glossary#offline-grace) appropriate to your risk.
2. Explain offline deadlines to your customers before purchase.
3. Test the actual packaged app on Windows, macOS and Linux versions you support.
4. Review token storage, file permissions and optional keyring behaviour.
5. Decide recovery behaviour for lost or copied application state.
Detail: [offline flow](/docs/v1/start/activation-validation-deactivation#offline-or-timeout) and [local state](/docs/v1/start/activation-validation-deactivation#local-state-and-os-differences).
Expected result: bounded offline access works as intended without claiming instantaneous disconnected revocation.
## Webhook operations
1. Use an HTTPS endpoint on your backend with the correct environment.
2. Store its signing secret privately and verify signatures and timestamp tolerance.
3. Deduplicate repeated event handling and tolerate ordering differences.
4. Monitor [delivery](/docs/v1/start/glossary#delivery) failures and exhausted retries.
5. Rehearse secret replacement; current queued attempts use the current endpoint secret.
Detail: [webhook summary](/docs/v1/start/core-concepts#record-reference) and [current operational limitations](/docs/v1/integration-notes).
Expected result: missing webhooks do not determine whether a license is valid. Follow the complete event/security reference when it is published; this checklist is not a replacement verifier implementation.
## Monitoring, limits and backup responsibility
1. Monitor API and audit logs, [request IDs](/docs/v1/start/glossary#request-id) and webhook delivery history.
2. Review plan creation limits and current usage before a sales launch.
3. Check [platform status](/status) and handle unavailable network requests in your app.
4. Protect backup copies of your own server credentials under your recovery policy; never ask customers to back up KDPika private signing keys.
5. Preserve nonsecret mappings between your orders, customers and issued license IDs.
6. Confirm scheduler, backup and restore operations for systems you operate rather than assuming an untested RPO or RTO.
Detail: [platform versus product capacity](/docs/v1/start/core-concepts#product-and-platform-plans) and [network outcomes](/docs/v1/start/activation-validation-deactivation#offline-or-timeout).
Expected result: operational failures can be diagnosed without exposing secrets. The same-host status page is not an independent monitor or an SLA.
## Sales, privacy and customer support
1. Confirm purchases in your own backend before issuing software licenses.
2. Document your refund, renewal, transfer and lost-key policies.
3. Give customers a publisher support channel and a recovery procedure.
4. Minimise customer information sent into metadata and labels.
5. Review your privacy notice, retention and the platform's processing terms.
6. Confirm the remaining hosting/provider/legal facts relevant to your own obligations.
Detail: [sales boundaries](/docs/v1/start/introduction#what-the-service-does-not-do), [use cases](/docs/v1/start/use-cases), [privacy](/legal/privacy) and [DPA](/legal/dpa).
Expected result: software buyers know whom to contact; KDPika platform billing is not confused with your product sales.
## Expected result
Each applicable check has an actual test or operational owner. Unresolved security/compatibility risks are documented and addressed before your application is declared production-ready.
## Common mistakes
- Checking only a successful activation on one development machine.
- Assuming a demo certifies every OS credential backend.
- Treating an advisory monthly API allowance as a hard block.
- Claiming compliance, response-time guarantees or uptime without confirmed evidence.
## Security notes
Do not disable verification to pass a release test. Fully controlled customer machines can patch local code. No checklist is an independent security audit or universal legal certification.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| A required test fails | Application or SDK is not ready for that case | Stop release for the affected path; investigate rather than bypassing it |
| Ownership of an operational check is unclear | No documented support/recovery process | Assign an owner and record the procedure |
| Need a capability not documented as supported | Product assumptions exceed actual behaviour | Ask support before committing to customers |
## Next steps
Review [use-case trade-offs](/docs/v1/start/use-cases) and resolve any blocked check with your engineering and support owners.
## Related
- [Quickstart](/docs/v1/start/quickstart)
- [Authentication](/docs/v1/start/authentication)
- [Current integration limitations](/docs/v1/integration-notes)
# Authentication
Canonical: https://licensing.kdpika.com/docs/v1/start/authentication
---
order: 9
slug: "/docs/v1/start/authentication"
title: "Authentication"
section: "Start here"
description: "Choose the correct public App ID, activation token or scoped developer API key and keep management secrets on trusted servers."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
Use a [public App ID](/docs/v1/start/glossary#public-app-id) for [activation](/docs/v1/start/glossary#activation), an [activation token](/docs/v1/start/glossary#activation-token) for an installation, and a [developer API key](/docs/v1/start/glossary#developer-api-key) only for server-side management.
## What you will learn
- Compare the three credential types.
- Create, [scope](/docs/v1/start/glossary#scope), rotate and revoke a management key.
- Respond to a leaked key without exposing more [customer](/docs/v1/start/glossary#customer) data.
## Prerequisites
- Read [core concepts](/docs/v1/start/core-concepts) and [environments](/docs/v1/start/environments).
- Use a verified workspace for dashboard actions.
## Credential comparison
| Credential | Who uses it / where it [lives](/docs/v1/start/glossary#live) | Shippable? | Scope and lifetime | Rotation |
| --- | --- | --- | --- | --- |
| Public App ID, app_ | App configuration; [Products](/docs/v1/start/glossary#product) card | Yes; not a secret | Identifies a product shared across environments; activation also needs a key and environment | No secret rotation is needed; no public-ID rotation workflow is offered |
| Activation token, act_ | One activated app installation; protected local storage | Never bundle a token for all customers | [License](/docs/v1/start/glossary#license)/[device](/docs/v1/start/glossary#device) binding; status-based acceptance, no independent elapsed-time TTL check; license and signed-cache deadlines still apply | Re-activate the installation; its prior active token is revoked |
| Developer API key, lmp_ | Your trusted backend secret configuration | No | [Organization](/docs/v1/start/glossary#organization), product, environment and scopes; configured expiry/revocation apply | Dashboard rotation immediately revokes the old key; no overlap window |
## Headers and environment
Activation sends X-App-ID and X-Environment: [SANDBOX](/docs/v1/start/glossary#sandbox) or LIVE. Its body contains the [license key](/docs/v1/start/glossary#license-key) and stable device_id. An optional email must match the license's customer.
Validation sends Authorization: Bearer followed by the activation token and X-Installation-ID containing the same persisted identity. Always send the installation header; the API currently accepts its omission, but then signs a null installation hash that conforming SDK checks do not accept.
Management sends Authorization: Bearer followed by the developer API key. That key, not an X-Environment override, determines the environment. Do not substitute an activation token into a management operation.
## Registered scopes
| Scope | Registered operations |
| --- | --- |
| licenses:read | Read a license by ID |
| licenses:write | Create, revoke, suspend, restore and renew licenses |
| devices:write | Reset devices for a license |
An arbitrary scope string does not create a new API permission. A reset-devices key does not gain license-creation rights.
## Create a key
1. Open API keys in the dashboard.
2. Choose the product and Sandbox or Live environment.
3. Select only the scopes your server needs.
4. Set a descriptive key name and any supported expiry.
5. Create the key and copy the full secret once into your server's secret configuration.
Expected result: the listing contains a display prefix, not a recoverable complete secret. Live issuance needs eligible paid access.
For client-only quickstart activation, do not create a management key at all.
## Rotate or revoke
1. Identify the intended key by its name, product and environment.
2. Use Rotate when replacing it, or Revoke when stopping all future use.
3. For rotation, copy the new secret and deploy it to the trusted backend.
4. Confirm a management request works with the replacement.
5. Review API and audit logs for unexpected failures.
Expected result: old-key requests fail. Rotation has no overlap; coordinate the backend update rather than assuming a zero-interruption grace window.
## Correct use by application
| App type | Correct | Incorrect |
| --- | --- | --- |
| Desktop | Public App ID and customer key activate; local token validates | Embed an lmp_ key in the executable |
| SaaS | Authenticate your user and gate server actions using valid license state | Trust a browser's paid=true claim |
| CLI | Persist identity and token in protected per-user state | Print a token into shell logs |
| Plugin or extension | Customer host uses activation credentials; your server manages licenses | Treat the customer's server or extension bundle as your secret vault |
## If a key leaks
1. Revoke the compromised key, or rotate it and securely deploy the replacement.
2. Review API and audit history for that key's product/environment and [request IDs](/docs/v1/start/glossary#request-id).
3. Identify affected licenses and investigate actual changes.
4. Suspend or revoke only licenses requiring that response under your policy.
5. Remove the exposed value from your release process and notify affected parties when appropriate.
Expected result: the old credential no longer works and the investigation is recorded. Deleting a public post alone does not invalidate a leaked key.
## Expected result
You can explain which secret is on the client, which is on the server, and how the selected environment is enforced.
## Common mistakes
- Treating the public App ID as a management secret.
- Rotating a [developer](/docs/v1/start/glossary#developer) key without updating its consumer.
- Omitting X-Installation-ID and accepting a signed response for no bound installation.
- Assuming a restored license revives its revoked activation tokens.
## Security notes
Developer and activation tokens are hashed in their corresponding database records. [Webhook](/docs/v1/start/glossary#webhook) secrets are a different credential, with an encrypted copy used for [delivery](/docs/v1/start/glossary#delivery). Do not put any of these secrets into documentation feedback.
A credential's successful authentication is not a promise that every resource-creation operation is allowed; endpoint scopes, current plan gates and license rules still apply.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| INVALID_API_KEY | Missing, expired, revoked or ineligible developer credential | Check the server's current key and environment |
| INSUFFICIENT_SCOPE | Key lacks the operation's registered scope | Issue an appropriately scoped key; do not broaden a shipped client |
| INVALID_TOKEN | Installation token is missing, replaced or invalidated | Re-activate only an eligible license |
| DEVICE_MISMATCH | Installation header differs from the token's binding | Load the original persisted ID instead of generating one per request |
## Next steps
Run the [quickstart](/docs/v1/start/quickstart), then read the [activation flow](/docs/v1/start/activation-validation-deactivation).
## Related
- [Environments](/docs/v1/start/environments)
- [License lifecycle](/docs/v1/start/license-lifecycle)
- [Current integration limitations](/docs/v1/integration-notes)
# License lifecycle
Canonical: https://licensing.kdpika.com/docs/v1/start/license-lifecycle
---
order: 10
slug: "/docs/v1/start/license-lifecycle"
title: "License lifecycle"
section: "Start here"
description: "Understand allowed license state transitions, expiry handling, renewal and the effects on active devices and tokens."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
[License](/docs/v1/start/glossary#license) status controls access, but status changes, expiry and device-seat release are distinct operations.
## What you will learn
- Recognize every stored license state.
- Choose allowed management transitions.
- Understand why restore and reset do not undo every counter or token change.
## Prerequisites
- Read [authentication](/docs/v1/start/authentication).
- Keep management keys only on a trusted server.
- Test transitions on a [Sandbox](/docs/v1/start/glossary#sandbox) license.
## States and transitions

Active licenses can be suspended and restored or revoked permanently. Expiry is checked during use and durably recorded by cron. Eligible expired licenses can be renewed. Pending and Cancelled are stored states with restricted operations.
| State | [Activation](/docs/v1/start/glossary#activation) / validation eligibility | Allowed management actions |
| --- | --- | --- |
| ACTIVE | Eligible if expiry, installation and applicable version rules pass | Suspend, revoke, restore as a same-state operation, renew |
| SUSPENDED | New activation rejected; active tokens revoked by suspension | Restore before expiry, repeat suspend, revoke, renew without removing suspension |
| PENDING | New activation rejected | Restore before expiry, revoke, renew without removing pending state |
| EXPIRED | Rejected | Renew to ACTIVE, revoke |
| REVOKED | Rejected; tokens invalidated | Repeated revoke only; no restore or renewal |
| CANCELLED | Rejected | No public cancel endpoint; no renewal or restore |
An elapsed expiry is checked before stored status during license eligibility. A future starts_at is currently included in signed data but is not enforced as a not-before boundary; do not use it to schedule a secure launch.
## Management operations
| Operation | API path after /api/v1 | Effects |
| --- | --- | --- |
| Suspend | POST /licenses/{license}/suspend | ACTIVE to SUSPENDED; invalidate active tokens, retain [device](/docs/v1/start/glossary#device) rows |
| Restore | POST /licenses/{license}/restore | Eligible SUSPENDED or PENDING to ACTIVE; old tokens stay invalid |
| Revoke | POST /licenses/{license}/revoke | Allowed non-cancelled states to terminal REVOKED; invalidate active tokens |
| Renew | POST /licenses/{license}/renew | Add 1–3650 days to future expiry or now; EXPIRED becomes ACTIVE |
| Reset devices | POST /licenses/{license}/reset-devices | Deactivate [active devices](/docs/v1/start/glossary#active-devices) and revoke active tokens; preserve license status and cumulative [activation count](/docs/v1/start/glossary#activation-count) |
Current registered management uses the [developer](/docs/v1/start/glossary#developer) key [scopes](/docs/v1/start/glossary#scope) described in authentication. There is no public status=PENDING or status=CANCELLED command. Creation produces an active license.
## Renewal details
Renewal refuses REVOKED and CANCELLED. A suspended or pending license keeps that state after renewal. A license with no expiry becomes time-limited when renewed with days.
Restore refuses an elapsed expiry; renew the eligible expiry first. [Product plans](/docs/v1/start/glossary#product-plan) marked monthly or yearly do not renew a [customer](/docs/v1/start/glossary#customer) license automatically when a payment occurs. Your own confirmed-payment backend must perform the intended licensing operation.
## Expiry and scheduled work
Elapsed expiry blocks eligibility even before the scheduler updates the stored status. The current validation transaction rolls back its attempted EXPIRED write when it throws a rejection; a failed request is not itself durable proof of an expiration event.
The platform cron process durably records expiry and marks active tokens expired. Event/email [delivery](/docs/v1/start/glossary#delivery) is queued operational work, not a prerequisite for denying an expired license.
A revoked, suspended or expired token can be rejected as INVALID_TOKEN. A fresh activation attempt can expose the corresponding license-state code. Do not assume every invalidation returns the same code on both paths.
## Devices, events and audit history
Suspend and revoke invalidate tokens but do not release device seats. Deactivate or reset devices releases active seats. Restore requires fresh activation; an already-active matching device does not consume another cumulative activation merely because its token is refreshed.
Actual transitions publish corresponding supported events and record audit history. Repeated same-state management operations can record repeated activity. Do not treat them as a universal exactly-once/idempotency guarantee.
## Expected result
You can suspend and restore an eligible Sandbox license, then re-activate it deliberately. Revocation is understood as permanent in the current lifecycle.
## Common mistakes
- Revoking when a temporary suspension was intended.
- Restoring an expired license without renewing its expiry.
- Resetting devices to clear cumulative activations.
- Assuming disabling a [product](/docs/v1/start/glossary#product) blocks every existing token: current validation does not enforce that product state consistently.
## Security notes
Offline cached access cannot instantly learn about a server-side change. It ends at the applicable signed deadline or learns the rejection when it reconnects. Never extend an offline allowance from editable local state.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| INVALID_STATE 409 | Requested transition is forbidden | Review the current state; do not retry a permanent revoke as restore |
| Old token still fails after restore | Restore does not resurrect tokens | Activate again with an eligible license and persisted identity |
| Seat remains after suspension | Suspension is not device deactivation | Review and deliberately release the correct installation |
| EXPIRED not visible immediately in the list | Cron has not persisted the transition | Check the actual expiry; validation still rejects elapsed licenses |
## Next steps
Read the [complete activation flow](/docs/v1/start/activation-validation-deactivation) and test its failure outcomes.
## Related
- [Core concepts](/docs/v1/start/core-concepts)
- [Authentication](/docs/v1/start/authentication)
- [Go live checklist](/docs/v1/start/go-live-checklist)
# Activation, validation and deactivation
Canonical: https://licensing.kdpika.com/docs/v1/start/activation-validation-deactivation
---
order: 11
slug: "/docs/v1/start/activation-validation-deactivation"
title: "Activation, validation and deactivation"
section: "Start here"
description: "Follow first-run, normal, offline, revoked, expired and device-limit flows with stable installation state and helpful user messages."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
Activate once for an installation, validate its current [license](/docs/v1/start/glossary#license) state, and deactivate when deliberately releasing that installation.
## What you will learn
- Follow six real outcomes without confusing transport success with authorization.
- Persist identity and token state safely.
- Explain failures to users without exposing credentials.
## Prerequisites
- Complete the [quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [license lifecycle](/docs/v1/start/license-lifecycle).
- Keep client time reasonably accurate and protect local application data.
## First run

App creates and saves a random installation ID, sends Sandbox activation with the public App ID and license key, verifies the signed result, then stores the token and gates features.
1. Generate a random [installation ID](/docs/v1/start/glossary#installation-id) once and persist it.
2. Ask for the [license key](/docs/v1/start/glossary#license-key) and, if your UX requires it, the [customer](/docs/v1/start/glossary#customer) email.
3. Send POST /api/v1/licenses/activate with X-App-ID and explicit X-Environment.
4. Verify the signed response and its [product](/docs/v1/start/glossary#product), environment, installation, status and deadlines.
5. Only after a valid result, persist the [activation token](/docs/v1/start/glossary#activation-token) and signed cache.
6. Require validity before enabling a [feature](/docs/v1/start/glossary#feature).
Expected result: one active installation and a token bound to its license/[device](/docs/v1/start/glossary#device).
The device_id must be a trimmed 16–255 character string. A random UUID meets this shape. Do not derive it from a MAC address, disk serial, username or unconsented tracking identifier.
## Normal run

App loads persisted identity/token, checks a verified cache before the signed next-validation time, or validates online with the same installation header, then gates features only for a valid result.
Before next_validation_at, [official SDKs](/docs/v1/start/glossary#official-sdk) can use a verified cache after semantic checks. Otherwise they request POST /api/v1/licenses/validate using the token and X-Installation-ID.
Re-activating the same active installation rotates its token without adding an active seat or another cumulative [activation](/docs/v1/start/glossary#activation). Do not activate on every launch when a valid stored token already exists.
## Offline or timeout

Online transport fails. App verifies the cached signature, installation, state, expiry, grace deadline and clock checkpoint. Only a valid bounded cache permits temporary access.
A transport failure can use a verified cache only within offline_grace_until and license expiry. A first activation cannot be completed offline. Missing, invalid or out-of-window cache must not unlock features.
The product defaults are 24 hours for normal validation and 168 hours for [offline grace](/docs/v1/start/glossary#offline-grace). Dashboard policy accepts 1–168 validation hours and 0–720 grace hours. Both deadlines are absolute offsets from signed server_time; grace is not added after the [validation interval](/docs/v1/start/glossary#validation-interval).
Force-online validation bypasses the early cache shortcut but can still fall back within grace on a transport failure. It is not a guaranteed internet-required/fail-closed switch.
Suggested user message: “We could not connect. You can continue until the date shown; please reconnect before then.” Show that date only from a valid signed allowance.
## Revoked or suspended

Server invalidates the activation token after suspension or revocation. A connected app receives rejection, discards trusted cached access and asks the customer to contact the publisher.
A server rejection is not an outage and does not qualify for offline fallback. Suspension/revocation invalidate active tokens; validation may return INVALID_TOKEN, while a new activation can return LICENSE_SUSPENDED or LICENSE_REVOKED.
Suggested message: “This license is not available. Please contact the application publisher.” Do not reveal whether another person's email or license exists.
A disconnected app may still use its previously signed allowance until it reconnects or reaches a deadline; revocation is not instantaneous offline.
## Expired

Signed expiry or server eligibility check rejects the license. App keeps paid features locked and offers the publisher's renewal route, not an unconditional retry loop.
A local signed expiry or server check can reject the license. Before cron, a check may return LICENSE_EXPIRED; after token invalidation, INVALID_TOKEN can be returned. Use the known signed state and confirmed server result, not a fabricated payment explanation.
Suggested message: “Your license has ended. Contact the publisher or renew through their purchase page.” KDPika platform checkout is not the publisher's software-buyer renewal page.
## Device or activation limit

New installation activation reaches the active device or cumulative activation cap. App refuses access; customer releases an old seat or asks the publisher to review policy.
DEVICE_LIMIT_REACHED 409 means active seats are full. ACTIVATION_LIMIT_REACHED 409 means cumulative activations are exhausted. Releasing a seat does not reset the cumulative count.
Suggested message: “This license has reached its installation limit. Remove an old installation or contact the publisher.” Display a self-service portal link only if the publisher supplied a valid one.
## Deactivation and reinstall
POST /api/v1/licenses/deactivate uses the activation token, deactivates its bound device and invalidates active tokens for that device. Repeat use of the old token returns INVALID_TOKEN, not a second successful operation.
Only clear local activation state after a confirmed success when the intention is to release a seat. Deleting local files alone does not tell the server that a device was released.
Persist the random identity in a stable per-user application-data directory across normal restarts. Reinstalling or losing that directory may create a new installation and require an explicit transfer/recovery policy.
## Local state and OS differences
| State | Storage rule |
| --- | --- |
| Installation ID | Persist once; reuse for activation and X-Installation-ID |
| Activation token | Protected per-user storage; never logs or shared bundles |
| Signed response | Verify on every use; cached JSON is not inherently trusted |
| Verification key | Trust HTTPS retrieval or a deliberately pinned public key; plan rotation |
| Clock checkpoint | Detect ordinary rollback; not trusted hardware or snapshot prevention |
Python can optionally use an OS keyring. PHP, JavaScript and C# use permission-protected files unless you add another layer. Windows ACLs, macOS Keychain and Linux credential-service availability need OS-specific testing. Do not promise native secure storage from a file-permission fallback.
## Expected result
You can draw the allowed access path and the refusal path for every outcome, and show plain user messages without overriding an invalid result.
## Common mistakes
- Generating a new device_id on every launch.
- Accepting features from a result whose valid flag is false.
- Treating SIGNATURE_INVALID as permission to ignore signatures.
- Confusing a timeout with a confirmed revoked-license decision.
- Claiming a local mutable clock checkpoint prevents every machine snapshot attack.
## Security notes
The outer success flag, token and [request ID](/docs/v1/start/glossary#request-id) are not a replacement for signed payload validation. Your app must enforce the feature gate. A patched local binary remains a limitation; server-side authorization is needed for valuable online resources.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| New device on every run | Identity not persisted | Reuse the installation file in a stable location |
| Another machine's token fails | Installation binding differs | Activate that installation under the customer's policy |
| Cache rejected in C# | Known Unicode serialization issue | Do not bypass verification; test online and consult compatibility notes |
| JS rejects price 19.00 | Integral-float canonicalization mismatch | Treat as SDK compatibility failure; consult notes before production |
| Offline access ends sooner than expected | Grace and expiry are absolute deadlines | Compare signed server_time, next_validation_at, offline_grace_until and expires_at |
## Next steps
Adapt the [use-case architecture](/docs/v1/start/use-cases) and complete the [go live checklist](/docs/v1/start/go-live-checklist).
## Related
- [Quickstart](/docs/v1/start/quickstart)
- [License lifecycle](/docs/v1/start/license-lifecycle)
- [Current integration limitations](/docs/v1/integration-notes)
# Use cases
Canonical: https://licensing.kdpika.com/docs/v1/start/use-cases
---
order: 12
slug: "/docs/v1/start/use-cases"
title: "Use cases"
section: "Start here"
description: "Compare practical licensing architectures for desktop apps, SaaS backends, command-line tools, plugins and extensions."
audience: "all"
level: "beginner"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"]
draft: false
---
Keep the [license](/docs/v1/start/glossary#license) decision close to the protected operation, and keep management credentials on infrastructure you control.
## What you will learn
- Choose an enforcement point for four application types.
- Identify the state each integration must persist.
- Find the matching foundation guide and executable example.
## Prerequisites
- Read [core concepts](/docs/v1/start/core-concepts).
- Choose a runtime from the [quickstart](/docs/v1/start/quickstart).
- Know which resources run on your server and which run on a [customer](/docs/v1/start/glossary#customer)'s machine.
## Desktop application
Recommended architecture: the application process handles [activation](/docs/v1/start/glossary#activation) and signed validation. Electron uses the main process, not a renderer containing management secrets.
Protect paid exports, batch operations or local premium tools with a valid result and the required entitlement. Do not check once in the activation dialog and forget to gate later actions.
Persist one [installation ID](/docs/v1/start/glossary#installation-id), a protected [activation token](/docs/v1/start/glossary#activation-token) and signed cache. Use configured offline deadlines rather than unconditional network dependence or unlimited offline access.
Trade-off: a local application can be patched. Valuable cloud actions still need authenticated server authorization.
Start with [Python](/docs/v1/start/quickstart#python-setup), [JavaScript](/docs/v1/start/quickstart#javascript-setup) or [C#](/docs/v1/start/quickstart#csharp-setup); follow [normal/offline flows](/docs/v1/start/activation-validation-deactivation).
## SaaS backend
Recommended architecture: authenticate your user, resolve their entitlement on your backend and check access before protected server operations. Your browser UI may display the result but must not be the authority.
Protect API actions, exports and server-owned resources, not just a hidden button. Bind license ownership to your authenticated account using your own server-side mapping.
Use a stable installation identity for the backend deployment or tenant integration policy you intentionally choose. Do not generate a [device](/docs/v1/start/glossary#device) ID per HTTP request or trust an ID submitted freely by an untrusted browser.
Management keys stay in your backend secret configuration. They create or renew licenses only after your separate sales flow confirms a purchase.
Trade-off: shared/multi-tenant backend identity and concurrency need your own design; there is no built-in end-user login system or floating-seat lease service for your SaaS customers.
Start with [PHP](/docs/v1/start/quickstart#php-setup) or [Node.js](/docs/v1/start/quickstart#javascript-setup), then [authentication](/docs/v1/start/authentication).
## CLI or developer tool
Recommended architecture: the tool offers an explicit activate command, checks before licensed actions and offers an explicit deactivate command.
Protect paid commands and online resources; show actionable nonzero exits for invalid licenses. Avoid putting a real key in a command-line argument that can enter shell history or process listings.
Persist the random installation identity and protected token under the current user's application-data directory. Separate state for different [products](/docs/v1/start/glossary#product) and environments.
Trade-off: headless machines may not have a credential service. Permission-protected files need OS review; an optional keyring backend is not guaranteed to exist.
Start with [Python](/docs/v1/start/quickstart#python-setup). The [cURL check](/docs/v1/start/quickstart#curl-setup) tests transport only, not a secure production client.
## Plugin or extension
Recommended architecture: activate the installed plugin through the client flow, and put license-management actions on your own trusted backend. A customer-controlled WordPress/server installation is still distributed software, not your secret vault.
Protect premium plugin operations locally and gate valuable online services on your backend. Browser extensions cannot safely conceal a management key in their bundle.
Persist an installation identity suitable for that installed instance. Document whether a staging copy, migration or reinstall should consume another seat; the server sees the identity supplied by your integration, not an infallible hardware fingerprint.
Trade-off: copied plugin state may carry the same installation identity. KDPika does not automatically detect every cloned host or supply a marketplace fulfilment plugin.
Start with [PHP](/docs/v1/start/quickstart#php-setup) for a PHP plugin, then read [device/activation outcomes](/docs/v1/start/activation-validation-deactivation#device-or-activation-limit).
## Deployment and sales boundaries
Your app's licensing environment, your sales provider's test mode and your KDPika [platform subscription](/docs/v1/start/glossary#platform-subscription) are different concerns. KDPika checkout does not collect money from your software buyers.
Use the [go live checklist](/docs/v1/start/go-live-checklist) for all four architectures. Store-specific implementation guides remain separate phase deliverables; this foundation does not claim an installed store integration.
## Expected result
You have an enforcement point, a state-storage plan and a clear distinction between application activation and server management.
## Common mistakes
- Treating the customer-hosted plugin as your trusted backend.
- Generating a new installation for every SaaS request.
- Protecting only front-end UI while leaving server endpoints unrestricted.
- Treating the CLI cURL smoke check as signature verification.
## Security notes
Do not distribute [developer API keys](/docs/v1/start/glossary#developer-api-key). Always guard entitlement access with a valid result. Test actual SDK serialization and cache behaviour before promising offline support in your product.
## Troubleshooting
| Symptom | Cause | Fix |
| --- | --- | --- |
| SaaS [feature](/docs/v1/start/glossary#feature) works through direct HTTP despite a disabled button | Backend lacks authorization | Gate the protected endpoint on your server |
| Headless CLI cannot unlock keyring | No usable OS credential backend | Review a protected-file strategy or install a suitable supported store |
| Staging copy consumes a seat | It has a different installation identity | Define and communicate your staging/transfer policy |
| Customer expects marketplace auto-fulfilment | No store backend integration was implemented | Connect your confirmed order flow deliberately |
## Next steps
Run the [quickstart](/docs/v1/start/quickstart) and complete the [go live checklist](/docs/v1/start/go-live-checklist).
## Related
- [Choose your path](/docs/v1/start/choose-your-path)
- [Authentication](/docs/v1/start/authentication)
- [Activation flow](/docs/v1/start/activation-validation-deactivation)
# Signed responses
Canonical: https://licensing.kdpika.com/docs/v1/security/signed-responses
---
title: "Signed responses"
description: "Understand the exact Ed25519 envelope, PHP canonical bytes, trusted keys, time checks and replay limits before accepting a license."
slug: "/docs/v1/security/signed-responses"
section: "Security"
order: 13
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/verify-ed25519","/docs/v1/security/test-vectors","/docs/v1/security/offline-mode"]
draft: false
---
Verify the signed payload with a trusted public key, then independently check whether its [license](/docs/v1/start/glossary#license) state permits the requested action.
## What you will learn
- Reproduce the exact signed bytes without losing numbers or Unicode.
- Bind a response to your [product](/docs/v1/start/glossary#product), environment and installation.
- Handle key changes, expiry, clock changes and replay honestly.
## Prerequisites
- Read [authentication](/docs/v1/start/authentication) and [the activation flow](/docs/v1/start/activation-validation-deactivation).
- Retain the original UTF-8 JSON response before a normal parser changes numeric tokens.
- Configure a trusted public key separately from the response being verified.
## Envelope and trust boundary
Activation and validation return a license envelope. A successful HTTP request alone is not proof of authenticity or permission. The signed part is only payload; activation_token, success, valid and request_id in the outer API response are not covered by this signature.
| Envelope field | Type | Required meaning |
| --- | --- | --- |
| payload | object | The license snapshot whose canonical UTF-8 bytes are signed |
| signature | string | Unpadded base64url representation of a 64-byte detached signature |
| algorithm | string | Must equal Ed25519; do not negotiate a different algorithm from the response |
| key_id | string | Must match the independently trusted key, not authorize downloading an arbitrary key URL |
The public key is 32 raw bytes, not a PEM file. The signature is 64 raw bytes, not a hex string. Ed25519 signs the message directly; do not pre-hash the payload or use Ed25519ph. See [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032.html).
## Exact signed bytes
The server recursively sorts associative PHP-array keys with ksort using SORT_STRING, preserves list order, then encodes the payload with JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE and JSON_PRESERVE_ZERO_FRACTION. The result has no indentation or separating whitespace. Strings and object names use UTF-8 byte ordering; JavaScript and .NET default UTF-16 ordering can differ for non-BMP keys.
Strings use literal Unicode, except U+2028 and U+2029 are escaped. Quotes, backslashes and JSON controls are escaped; forward slashes remain literal. There is no Unicode normalization. A composed character and its decomposed form are different bytes. Duplicate object names are not a supported wire format; reject them before signature checking.
An integral float such as 19.0 remains 19.0. Numeric encoding is PHP-specific and also depends on its serialize_precision setting. This contract is not RFC 8785 / JSON Canonicalization Scheme. Normal JSON.parse followed by JSON.stringify changes some signed numbers; sorting a normal parsed JavaScript object is insufficient. The [PHP JSON documentation](https://www.php.net/manual/en/function.json-encode.php) describes these flags and precision behavior.
The verification samples retain numeric tokens from the original server JSON and canonicalize its keys and strings. They do not reconstruct PHP floating-point formatting from rounded language numbers. Keep those original numeric tokens in any local cache too. An intermediary that changes a numeric representation can cause a safe verification failure. Do not bypass that failure.
## Signed payload field reference
Nullable means the field may explicitly contain null. A missing required field is not the same as null. Your application must validate the fields it uses after verifying the signature.
| Field | Type | Meaning and handling |
| --- | --- | --- |
| license_id | string | Public license identifier; compare with the expected license when known |
| product_id | string | [Public App ID](/docs/v1/start/glossary#public-app-id); must equal your configured product |
| product | string | Product display name; not an authorization identifier |
| plan | string | Product-plan slug; not the [developer](/docs/v1/start/glossary#developer)'s [platform subscription](/docs/v1/start/glossary#platform-subscription) |
| status | string | Current license state; require active for normal licensed access |
| environment | string | [sandbox](/docs/v1/start/glossary#sandbox) or [live](/docs/v1/start/glossary#live); must match the configured environment |
| license_key_mask | string | Display mask; never a credential |
| starts_at | string or null | UTC ISO 8601 start date; do not grant use before a future start |
| expires_at | string or null | UTC ISO 8601 expiry; null has no license expiry deadline |
| created_at | string or null | Creation display timestamp; not a freshness proof |
| max_devices | integer | Concurrent active-device limit |
| max_activations | integer or null | Cumulative activation limit; null means no configured cumulative limit |
| activation_count | integer | Cumulative [activation count](/docs/v1/start/glossary#activation-count) |
| active_devices | integer | Active count at snapshot time; not a live counter offline |
| min_version | string or null | Configured lower application-version restriction |
| max_version | string or null | Configured upper application-version restriction |
| [customer](/docs/v1/start/glossary#customer) | object | Customer information; treat as personal data |
| customer.id | string | Customer's public identifier |
| customer.name | string or null | Optional name |
| customer.email | string | Customer email associated with the license |
| customer.company | string or null | Optional company |
| customer.country | string or null | Optional country |
| product_details | object | Product display metadata |
| product_details.id | string | Public App ID, consistent with product_id |
| product_details.name | string | Product display name |
| product_details.platform | string | Lowercase configured application platform |
| plan_details | object | Product-plan snapshot |
| plan_details.id | string | Product-plan identifier |
| plan_details.name | string | Product-plan display name |
| plan_details.slug | string | Product-plan slug |
| plan_details.billing_type | string | Lowercase product-plan billing type |
| plan_details.price | number | PHP float; 19.0 and 19 have different signature bytes |
| plan_details.currency | string | Product-plan currency; not evidence of a payment |
| plan_details.duration_days | integer or null | Configured license duration |
| plan_details.trial_days | integer or null | Configured trial duration |
| [features](/docs/v1/start/glossary#feature) | object or empty array | Dynamic feature keys; empty PHP feature arrays can serialize as [] |
| features[key] | JSON value | Boolean or configured JSON value; validate its type before use |
| [device](/docs/v1/start/glossary#device) | object | Bound installation metadata |
| device.id | string | Device's public identifier |
| device.installation_hash | string or null | SHA-256 of the persisted [installation ID](/docs/v1/start/glossary#installation-id); require a match |
| device.name | string or null | Optional installation label |
| device.platform | string or null | Optional platform description |
| device.app_version | string or null | Optional application version description |
| server_time | string | UTC ISO 8601 snapshot time; signed, but still replayable |
| next_validation_at | string | Suggested online-validation deadline |
| offline_grace_until | string | Absolute offline-fallback deadline |
Activation can return empty device-description strings; validation can return null for the same absent descriptions. Null is signed data, not a reason to remove a property before verification. Omitting X-Installation-ID during validation can produce a null installation hash; always send the persisted ID and reject an unbound response in your client.
## Fetch and cache the trusted public key
1. Fetch GET /api/v1/public-key from the configured KDPika HTTPS origin, using normal certificate and hostname verification.
2. Read key.algorithm, key.key_id, key.public_key and key.encoding from the successful response.
3. Require Ed25519, base64url and a decoded 32-byte key.
4. Calculate key_id as ed25519_ followed by the first 16 lowercase hexadecimal characters of SHA-256 of those raw public bytes.
5. Pin the approved key in your application or save the HTTPS-fetched key in protected application state according to your trust policy.
6. Cache the current key for at most the endpoint's one-hour cache lifetime before an intentional refresh.
Expected result: a response cannot choose its own key, host or algorithm. An untrusted file beside the application is not a secure pin merely because its filename is key.json.
Trust on first use from HTTPS depends on the origin, certificate validation and the client's local environment. A compiled pin reduces dependence on a later key fetch, but a fully controlled client machine can still replace the program. Do not fetch a key using a URL supplied in an envelope.
## Key changes and rotation
The platform currently exposes one configured public key and reads one signing-key pair. There is no public historical key ring, overlapping-signature period, key-validity schedule or rotation-history endpoint.
1. Treat an unknown key_id as untrusted, not as permission to skip verification.
2. If your policy allows refresh, fetch once from the fixed HTTPS public-key endpoint and validate its format and identity.
3. Accept a new key only through your intended trust-update policy; a strictly pinned client needs an application/configuration rollout.
4. Retry verification once with the approved key or return a clear connection/update-required error.
5. Discard old cached signed states according to the rollout and expiry policy.
Expected result: rotation is deliberate and failures remain closed. Any client-managed overlap is your integration policy, not an existing server guarantee. Compromise of a signing key requires a separate coordinated response with KDPika support.
## Time, binding and replay
After authenticity succeeds, require the intended product, environment and installation hash, active status, acceptable application version, valid start/expiry and the appropriate signed validation/grace deadlines. Use the [official SDK](/docs/v1/start/glossary#official-sdk)'s valid result as the first gate before feature helpers.
Both next_validation_at and offline_grace_until are calculated from server_time. Grace is not automatically added after the [validation interval](/docs/v1/start/glossary#validation-interval). A valid signature can belong to an expired license: the expired vector intentionally demonstrates this distinction.
The payload contains no signed request nonce, client challenge or monotonic sequence number. A previously valid signed snapshot can be replayed until your time/binding policy rejects it. The SDKs use a five-minute local clock-rollback tolerance, but local files and clocks are mutable. Revocation on the server cannot instantly invalidate a disconnected cached response. Shorter intervals and server-side authorization reduce exposure; they do not make a local binary unpatchable.
The current source has a recorded database-timezone discrepancy for created_at on a non-UTC local database. Do not use created_at as proof of freshness. This documentation does not change stored timestamps or signing behavior.
## Expected result
The positive vector verifies. A changed payload or wrong key fails. The expired vector verifies cryptographically but must fail your time-based access decision. A response for another product, environment or installation does not unlock your application.
## Common mistakes
- Signing the outer response, or signing an already rounded JavaScript number tree.
- Using a public key embedded in the same untrusted response as the trust anchor.
- Removing nulls, normalizing Unicode or rewriting numbers before checking the signature.
- Treating signature_valid as permission to use an expired or revoked snapshot.
## Security notes
The public signature prevents undetected edits under a trusted key. It does not encrypt customer information, confirm a buyer payment, make the local clock trustworthy or prove a response is the newest available state. Keep raw caches private and avoid logging customer payloads or [activation tokens](/docs/v1/start/glossary#activation-token).
## Troubleshooting
| Symptom | Likely cause | Safe next step |
| --- | --- | --- |
| Only integral-price responses fail | A JSON round trip changed 19.0 to 19 | Preserve original numeric tokens; run the integral-float vector |
| Unicode works online but not from cache | The cache changed string escaping or the client canonicalizer preserved escaped raw strings | Run Unicode vectors; reject the cached result and investigate the known SDK discrepancy |
| New responses use an unknown key_id | A key changed or the response is untrusted | Follow the deliberate key-refresh/pin-update policy; never bypass verification |
| Signature passes but access fails | Expiry, status, environment, product or installation checks rejected it | Explain the actual license state to the customer and validate online when appropriate |
## Next steps
Use [the complete verification samples](/docs/v1/security/verify-ed25519), then run [every test vector](/docs/v1/security/test-vectors).
## Related
- [Offline mode and grace](/docs/v1/security/offline-mode)
- [Entitlements and features](/docs/v1/security/entitlements-and-features)
- [Threat model](/docs/v1/security/threat-model)
# Verify Ed25519 signatures
Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519
---
title: "Verify Ed25519 signatures"
description: "Run independent signature verifiers in fourteen languages with the same lossless JSON contract and shared conformance tests."
slug: "/docs/v1/security/verify-ed25519"
section: "Security"
order: 14
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Choose a complete language example, verify every public test vector, then implement the separate [license](/docs/v1/start/glossary#license) access checks.
## What you will learn
- Select a verifier for your runtime.
- Build full source with pinned dependencies.
- Distinguish standalone verification from [official SDK](/docs/v1/start/glossary#official-sdk) behavior.
## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Have a trusted public key and the original UTF-8 envelope.
- Install the runtime described on the selected language page.
## Choose your language
| Language | Runtime / dependencies | Complete example |
| --- | --- | --- |
| Python | Python 3.9+; cryptography 46.0.3 | [Python source and commands](/docs/v1/security/verify-ed25519/python) |
| PHP | PHP 8.2+ with ext-sodium; no Composer dependency | [PHP source and commands](/docs/v1/security/verify-ed25519/php) |
| JavaScript | Node.js 22+; built-in crypto; no npm dependency | [JavaScript source and commands](/docs/v1/security/verify-ed25519/javascript) |
| TypeScript | Node.js 22+; TypeScript 7.0.2; @types/node 22.19.7 | [TypeScript source and commands](/docs/v1/security/verify-ed25519/typescript) |
| C# | .NET 7+; BouncyCastle.Cryptography 2.7.0 | [C# source and commands](/docs/v1/security/verify-ed25519/csharp) |
| Java | JDK 17+; Gson 2.14.0; Bouncy Castle 1.86 | [Java source and commands](/docs/v1/security/verify-ed25519/java) |
| Kotlin | Kotlin compiler 2.4.21; JDK 17+; Gson 2.14.0; Bouncy Castle 1.86 | [Kotlin source and commands](/docs/v1/security/verify-ed25519/kotlin) |
| Swift | Swift 6+; macOS 13+; built-in CryptoKit | [Swift source and commands](/docs/v1/security/verify-ed25519/swift) |
| Go | Go 1.22+; standard library only | [Go source and commands](/docs/v1/security/verify-ed25519/go) |
| Rust | Rust 1.81+; exact dependencies in Cargo.toml and included Cargo.lock | [Rust source and commands](/docs/v1/security/verify-ed25519/rust) |
| Ruby | Ruby 2.6+; libsodium 1.0.20; standard Ruby Fiddle/JSON | [Ruby source and commands](/docs/v1/security/verify-ed25519/ruby) |
| Dart | Dart 3.7+; cryptography 2.9.0 | [Dart source and commands](/docs/v1/security/verify-ed25519/dart) |
| C | C11 compiler; yyjson 0.13.0; libsodium 1.0.20 | [C source and commands](/docs/v1/security/verify-ed25519/c) |
| C++ | C++17 compiler; yyjson 0.13.0; libsodium 1.0.20 | [C++ source and commands](/docs/v1/security/verify-ed25519/cpp) |
## Download and run
1. Download [the complete source archive](/docs/downloads/security-verifiers.zip).
2. Extract it and open a terminal in its security directory.
3. Build the selected language using its full instructions.
4. Run the positive, tampered and expired commands.
5. Run selftest.py with that language's command to verify all 23 cases.
Expected result: PASS23. Every language was executed on macOS against the same23 cases,322 checks in total. These tests cover authenticity and malformed-input refusal, not native Windows/Linux packaging or a complete license policy.
## What the samples preserve
Numeric tokens remain in their original server form, including 19.0, large integers and PHP exponent notation. Object names sort by UTF-8 bytes; Unicode strings are not normalized. Duplicate keys, invalid Unicode, trailing content, unsupported algorithms, bad key IDs, malformed signatures and excessive depth fail closed.
The trusted key is a separate input. Do not extract a public key from an envelope and treat it as trusted. The programs return signature_valid only; a cryptographically valid expired vector is still not usable.
## Current SDK limitations
These independent examples do not change the official SDKs. The current JavaScript SDK can lose integral-float markers. C# can fail after escaped-Unicode cache serialization. Python can reject valid signatures containing Unicode separators or PHP exponent spellings. Numeric [feature](/docs/v1/start/glossary#feature) helpers in all SDKs require an explicit valid-result guard. Test your exact payloads and cache round trips before a release; do not disable signature verification to work around a discrepancy.
The four official SDKs still provide [activation](/docs/v1/start/glossary#activation), binding and offline-policy behavior that these small verifiers intentionally do not implement. A custom client needs the complete [signed-response checks](/docs/v1/security/signed-responses) and [offline policy](/docs/v1/security/offline-mode), not just this cryptographic function.
## Expected result
Every selected verifier returns the documented result for all 23 vectors. Actual license permission remains a separate decision.
## Common mistakes
- Unlocking on signature_valid alone.
- Shipping the synthetic vector key as a production key.
- Calling a standalone verifier an official SDK or silently replacing SDK internals.
## Security notes
Keep dependencies pinned and review updates. Do not pass raw [customer](/docs/v1/start/glossary#customer) envelopes or tokens to public debugging services. The sample archive contains only public synthetic values.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Shared runner fails | Runtime, source or canonical bytes differ | Read the failing vector name; rebuild exact source |
| Signature works but SDK fails | Different SDK canonical/cache implementation | Reproduce with the public cases and contact support |
| Wrong-language build instructions | Dependencies differ by runtime | Open the matching language page |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Complete Integration Guide
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide
---
slug: "/docs/v1/integration-guide"
title: "Complete Integration Guide"
description: "Follow fourteen ordered stages from your first Sandbox account to an assessed, safely gated production integration."
section: "Integration Guide"
order: 15
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## Your integration roadmap
Start at Stage 1, or skip to the task you need. Times are estimates after your runtime and email are available; they are not completion guarantees.
1. [Stage 1: Understand the idea](/docs/v1/integration-guide/understand) — 5 minutes · beginner.
2. [Stage 2: Set up your Sandbox](/docs/v1/integration-guide/sandbox) — 10 minutes · beginner.
3. [Stage 3: Create your product and plan](/docs/v1/integration-guide/model) — 10 minutes · beginner.
4. [Stage 4: Create a test license](/docs/v1/integration-guide/license-and-credentials) — 5 minutes · beginner.
5. [Stage 5: Choose your language and app type](/docs/v1/integration-guide/choose) — 3 minutes · beginner.
6. [Stage 6: Install and configure](/docs/v1/integration-guide/install) — 10 minutes · beginner.
7. [Stage 7: Activate your first license](/docs/v1/integration-guide/activate) — 5 minutes · beginner.
8. [Stage 8: Validate when your app starts](/docs/v1/integration-guide/validate) — 10 minutes · intermediate.
9. [Stage 9: Check the signed decision](/docs/v1/integration-guide/signatures) — 10 minutes · intermediate.
10. [Stage 10: Handle errors and offline use](/docs/v1/integration-guide/real-world) — 15 minutes · intermediate.
11. [Stage 11: Protect tokens and identity](/docs/v1/integration-guide/storage) — 15 minutes · intermediate.
12. [Stage 12: Deliver licenses to customers](/docs/v1/integration-guide/deliver) — 15 minutes · beginner.
13. [Stage 13: Test the complete lifecycle](/docs/v1/integration-guide/test) — 20 minutes · intermediate.
14. [Stage 14: Go live safely](/docs/v1/integration-guide/go-live) — 15 minutes · intermediate.
## What you will build
Your app activates a customer key, validates signed access before protected work and handles [device](/docs/v1/start/glossary#device) changes and failures. You can issue keys manually or connect a verified sales workflow.
Choose Desktop app, Web / SaaS backend, CLI tool, Plugin / extension, or manual issuance. Select your language at [Choose your path](/docs/v1/start/choose-your-path). The guide supports four official SDKs and ten REST client routes.
## Choose a support level
Python, JavaScript/Node.js, PHP and C# use official source SDKs. Ten other languages have REST client projects, not [official SDKs](/docs/v1/start/glossary#official-sdk). A REST transport response alone must not grant access.
## Before you go live
Runtime, signature/canonicalization and storage limitations must be resolved for your real payloads. [Current integration limitations](/docs/v1/integration-notes) are part of the launch gate, not optional reading after release.
## Next steps
[Start Stage 1: Understand the idea](/docs/v1/integration-guide/understand)
# Signature test vectors
Canonical: https://licensing.kdpika.com/docs/v1/security/test-vectors
---
title: "Signature test vectors"
description: "Download real public-safe Signer vectors and test authenticity separately from expiry, trust and license policy."
slug: "/docs/v1/security/test-vectors"
section: "Security"
order: 15
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Use synthetic, reproducible signed payloads to check whether your verifier accepts authentic bytes and rejects unsafe input.
## What you will learn
- Download the public keys and envelopes.
- Interpret positive, negative and expired expectations.
- Run one repeatable self-test without network calls.
## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Build [a complete verifier](/docs/v1/security/verify-ed25519).
- Never replace your production trust anchor with a test key.
## Vector catalogue
The [machine-readable index](/docs/vectors/index.json) lists each trusted-key filename, envelope filename and expected cryptographic result. Download [all files and the runner](/docs/downloads/security-verifiers.zip), or use the individually linked JSON below. Keys and payloads are synthetic; no private key or [activation token](/docs/v1/start/glossary#activation-token) is included.
| Case | Signature | [License](/docs/v1/start/glossary#license) permission | Input |
| --- | --- | --- | --- |
| positive | true | Active at the captured server time with the documented binding | [Envelope](/docs/vectors/positive.envelope.json) |
| integral-float | true | Active at captured time; preserves 19.0 | [Envelope](/docs/vectors/integral-float.envelope.json) |
| tampered | false | Reject | [Envelope](/docs/vectors/tampered.envelope.json) |
| wrong-key | false | Reject with the separately supplied wrong trusted key | [Envelope](/docs/vectors/wrong-key.envelope.json) |
| expired | true | Reject: expiry is in 2020 | [Envelope](/docs/vectors/expired.envelope.json) |
| unicode | true | Canonicalization test, not a complete license | [Envelope](/docs/vectors/unicode.envelope.json) |
| numbers | true | Canonicalization test, not a complete license | [Envelope](/docs/vectors/numbers.envelope.json) |
| key-order | true | Canonicalization test, not a complete license | [Envelope](/docs/vectors/key-order.envelope.json) |
| normalization | true | Two distinct Unicode names remain distinct | [Envelope](/docs/vectors/normalization.envelope.json) |
| nested | true | Lists, nulls and nested objects preserve their types/order | [Envelope](/docs/vectors/nested.envelope.json) |
| wrong-algorithm / wrong-key-id | false | Reject untrusted envelope metadata | See index |
| short-signature / padded-signature | false | Reject malformed base64url/signature | See index |
| missing-payload / array-payload | false | Reject malformed envelope | See index |
| duplicate-property / escaped-duplicate | false | Reject ambiguous JSON names | See index |
| trailing-content / invalid-unicode / depth-limit | false | Reject malformed or excessive input | See index |
| replacement-surrogate / replacement-invalid-utf8 | false | Reject malformed input instead of silently repairing it to a valid signed replacement character | See index |
## Captured public key
[positive.key.json](/docs/vectors/positive.key.json) is the separately trusted test anchor for the captured positive response.
```json
{"algorithm":"Ed25519","key_id":"ed25519_c8beae78c32584df","public_key":"HC1rqDdeN-nHyHTRmNL3s_sVfhtD6VOk6INbzSR26UA","encoding":"base64url"}
```
## Complete captured envelope
The positive response was captured from a real isolated local [Sandbox](/docs/v1/start/glossary#sandbox) [activation](/docs/v1/start/glossary#activation). Its dates describe that capture, not a currently valid purchase. The expired and stress cases were signed by the unchanged server Signer using isolated synthetic keys; they are not claimed successful API activations of expired licenses.
```json
{"payload":{"license_id":"lic_18d0daa92fddced51a44ed14","product_id":"app_f6cc3e1e2b2db33273b3","product":"Docs fixture product","plan":"fixture-plan","status":"active","environment":"sandbox","expires_at":"2026-11-13T17:43:50+00:00","license_key_mask":"TESTDOCS-••••-••••-7CCL","starts_at":"2026-10-14T17:43:50+00:00","created_at":"2026-10-14T19:43:50+00:00","max_devices":2,"max_activations":4,"activation_count":1,"min_version":null,"max_version":null,"active_devices":1,"customer":{"id":"cus_9df14dc98f63999a6adf69c8","name":"Synthetic Example User","email":"customer@example.invalid","company":null,"country":null},"product_details":{"id":"app_f6cc3e1e2b2db33273b3","name":"Docs fixture product","platform":"desktop"},"plan_details":{"id":"plan_4de2dfbe92a4aac4b0e880ac","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_af3087ec2eeb536a4f4c61a7","installation_hash":"d46f8ccf48213ab042bf83539f20aae1bd14056475471aa40b32e2565ad0ea73","name":"Synthetic local fixture","platform":"test","app_version":"1.0.0"},"server_time":"2026-10-14T17:43:50+00:00","next_validation_at":"2026-10-15T17:43:50+00:00","offline_grace_until":"2026-10-21T17:43:50+00:00"},"signature":"TvLKHsvFR2O2ip13W2XqwMjA_PyUa7Q26TiVL5LcAS7nIFGOUuuU6oto9lAhYLwwyQoWaKls5sUFWLgOzdkgAg","algorithm":"Ed25519","key_id":"ed25519_c8beae78c32584df"}
```
## Repeatable procedure
1. Extract the archive into a new directory.
2. Open the selected [language page](/docs/v1/security/verify-ed25519) and build its verifier.
3. Run the shared runner with --command followed by the verifier command and no input filenames.
4. Confirm PASS23 and exit 0.
5. Change one byte in a copy of a valid payload and verify refusal.
6. Run the expired vector and confirm signature_valid=true, then reject it in your expiry policy.
Expected result: every signature expectation matches; the runner treats a required refusal as a passing test. It does not compare a test vector's claimed result to itself without executing your verifier.
## Shared test runner
```python
"""Run every public vector using your verifier; Python 3.9+, standard library.
Run: python3 selftest.py --command python3 python/verify.py
Expected: PASS23 and exit0. A mismatched result prints FAIL and exits1.
No network, database, credentials, SDK monkey-patching or access decision.
"""
import argparse
import json
import pathlib
import subprocess
import sys
parser = argparse.ArgumentParser()
parser.add_argument('--command', nargs=argparse.REMAINDER, required=True)
args = parser.parse_args()
root = pathlib.Path(__file__).resolve().parent
vectors = json.loads((root / 'vectors/index.json').read_text())['vectors']
failures = []
for row in vectors:
command = args.command + [str(root / 'vectors' / row['key']), str(root / 'vectors' / row['envelope'])]
try:
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
expected = row['signature_valid']
ok = (result.returncode == (0 if expected else 1)
and result.stdout.strip() == 'signature_valid=' + str(expected).lower()
and not result.stderr)
except (OSError, subprocess.TimeoutExpired):
ok = False
if not ok:
failures.append(row['name'])
if failures:
print('FAIL: ' + ', '.join(failures))
sys.exit(1)
print('PASS' + str(len(vectors)))
```
## Why expiry is not a negative signature
Ed25519 proves authenticity of bytes. It does not read the clock. The expired case is deliberately signature-positive and authorization-negative. Your app must also enforce the expected [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry and validation/grace deadlines. Stress cases lack complete license fields and must never grant access.
The captured [installation ID](/docs/v1/start/glossary#installation-id) is the public synthetic value p1-installation-0000000001. Semantic checks use its SHA-256 and the captured server_time; the runner itself tests only authenticity.
## Expected result
All 23 cases match the index in every listed language; expired is authentic but unusable.
## Common mistakes
- Expecting an authentic expired signature to fail cryptographically.
- Running a vector with another case's key file.
- Editing numbers while formatting the envelope.
## Security notes
Treat all vector keys as disposable test anchors. Malformed-vector files intentionally contain invalid JSON or duplicate names; refusal is required. Never import their payloads as real [customers](/docs/v1/start/glossary#customer) or licenses.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Positive fails | Wrong key or changed representation | Use the exact pair named in index.json |
| Expired returns true | Correct authenticity result | Apply expiry separately |
| Duplicate case cannot parse | Expected security refusal | Preserve the nonzero result |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in Sandbox.
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Understand the idea
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/understand
---
slug: "/docs/v1/integration-guide/understand"
title: "Understand the idea"
description: "Stage 1: understand the idea with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 16
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
A [license key](/docs/v1/start/glossary#license-key) is like a ticket. Your software checks the ticket before allowing a protected [feature](/docs/v1/start/glossary#feature).
## What you will do
Understand the four actions without writing code.
An API is the HTTPS interface your program calls. An SDK is a ready-made client library that makes those calls and checks responses. A signed response includes proof that the server's access decision was not changed.
Customer buys → gets a license key → your app activates → your app checks access.
1. Your [customer](/docs/v1/start/glossary#customer) receives a key, manually or after a verified purchase.
2. **Activate** connects that key to one installation: one installed copy of your app.
3. **Validate** asks whether that installation still has access.
4. **Deactivate** releases its [device](/docs/v1/start/glossary#device) seat when moving to another computer.
A device seat is one allowed installation. An entitlement is a feature or limit, such as PDF export or two devices.
## Expected result
You can explain why receiving a key is not the same as permission to unlock a feature.
## If it does not work
If you only want to send keys, skip coding and use the non-programmer route in [Stage 12](/docs/v1/integration-guide/deliver).
## Stage checklist
- I know the difference between a key, an installation and a signed access decision.
## Next steps
[Continue to Stage 2: Set up your Sandbox](/docs/v1/integration-guide/sandbox)
# Offline mode and grace
Canonical: https://licensing.kdpika.com/docs/v1/security/offline-mode
---
title: "Offline mode and grace"
description: "Configure real SDK cache and grace behavior, test connection failures safely and understand revocation and clock limitations."
slug: "/docs/v1/security/offline-mode"
section: "Security"
order: 16
audience: "all"
level: "advanced"
last_updated: "2026-10-21"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Allow bounded offline use only from a verified cache, never as a fallback after the server rejects a [license](/docs/v1/start/glossary#license).
## What you will learn
- Configure [product](/docs/v1/start/glossary#product) validation/grace policy and all four SDK clients.
- Distinguish cached, online, offline and refused states.
- Test expiry, clock rollback and server rejection.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [signed responses](/docs/v1/security/signed-responses).
- Use a separate test license, private state directory and trusted test clock.
## 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.
| Product setting | Default | Dashboard accepted range | Meaning |
| --- | --- | --- | --- |
| validation_interval_hours |24 hours |1–168 hours | next_validation_at = server_time + this duration |
| offline_grace_hours |168 hours |0–720 hours | offline_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.
| State | Actual SDK behavior | User-facing approach |
| --- | --- | --- |
| No [activation token](/docs/v1/start/glossary#activation-token) | NOT_ACTIVATED; online [activation](/docs/v1/start/glossary#activation) is required | Ask for the [customer](/docs/v1/start/glossary#customer)'s [license key](/docs/v1/start/glossary#license-key) |
| Verified cache before next_validation_at | Return source=cache without a validation call | Continue authorized [features](/docs/v1/start/glossary#feature) |
| Online acceptance | Verify response and refresh signed state | Continue; show no unnecessary technical message |
| Transport failure within grace/expiry | Reverify cached state; source=offline | Explain temporary offline use and reconnect deadline |
| Server rejection | Clear signed state; do not use grace | Explain the actual refusal and offer support |
| Missing/tampered cache | OFFLINE_CACHE_MISSING or OFFLINE_CACHE_INVALID | Ask the user to reconnect |
| Grace ended | OFFLINE_GRACE_EXPIRED | Ask the user to connect to continue |
| Clock more than5 minutes behind latest checkpoint | CLOCK_ROLLBACK_DETECTED | Ask 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 type | Starting interval / grace | Trade-off |
| --- | --- | --- |
| Offline-friendly desktop |24 /168 hours | Default availability; revocation can take up to the cached allowance to reach a disconnected app |
| High-value connected desktop |1 /24 hours | More calls and shorter stale-access window |
| Online SaaS backend |1 /0 hours | Gate valuable server actions online; add your own explicit fail-closed policy |
| CLI / build tool |24 /72 hours | Predictable build availability; avoid logging credentials |
| Customer-hosted plugin |24 /168 hours | User 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](/docs/v1/security/verify-ed25519); run your actual payload/cache round trips before shipping.
## Complete SDK configuration
Install SDK1.0.0 using [the verified quickstart instructions](/docs/v1/start/quickstart). 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](/docs/v1/sdks/python). 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
"""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
// 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
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.
```xml
Exenet7.0enableenable
```
```csharp
// 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>(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
| Symptom | Cause | Action |
| --- | --- | --- |
| Offline fails immediately | Missing/unverifiable cache or expired deadline | Reconnect and validate; do not ignore signature errors |
| Revoked license still works while disconnected | Old signed snapshot remains inside grace | Use a shorter product policy or server-side features |
| Policy edit appears ineffective | Cached deadlines came from an earlier response | Validate online and inspect the new signed deadlines |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in Sandbox.
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Entitlements and features
Canonical: https://licensing.kdpika.com/docs/v1/security/entitlements-and-features
---
title: "Entitlements and features"
description: "Define product-plan features, enforce boolean and numeric entitlements safely and understand cached plan changes."
slug: "/docs/v1/security/entitlements-and-features"
section: "Security"
order: 17
audience: "all"
level: "advanced"
last_updated: "2026-10-18"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Gate each [feature](/docs/v1/start/glossary#feature) on a valid [license](/docs/v1/start/glossary#license) result and the expected feature type, not on a plan name or truthy value alone.
## What you will learn
- Define plan features and read their signed values.
- Enforce numeric limits without coercion.
- Version feature names and handle changed plans.
## Prerequisites
- Complete [the quickstart](/docs/v1/start/quickstart).
- Understand [product plans](/docs/v1/start/core-concepts).
- Use the [guarded SDK configuration programs](/docs/v1/security/offline-mode#complete-sdk-configuration).
## Define product-plan features
1. Open Plans & features in your workspace.
2. Select the intended [product](/docs/v1/start/glossary#product) and create a product plan; the current screen has no general edit-plan form.
3. Add a stable feature key such as pdf_export to its comma-separated enabled feature keys.
4. Create the plan and issue a synthetic [Sandbox](/docs/v1/start/glossary#sandbox) license for that plan.
5. Activate or validate the license and inspect the verified result's features.
Expected result: the signed payload contains the plan's feature keys. A [platform subscription](/docs/v1/start/glossary#platform-subscription) such as Starter or Pro controls your KDPika workspace; it is not the product plan you sell to your buyers.
The server selects plan_features and joins license_features overrides for those same plan feature IDs. A configured value_json takes precedence over the boolean enabled value. A license-only override for a feature absent from the plan query does not automatically add a new response key. Empty features can serialize as [].
## Types and safe decisions
| Feature value | Intended application decision | Unsafe alternative |
| --- | --- | --- |
| pdf_export: true | Require a valid result and an actual boolean true | Unlock because the key exists |
| pdf_export: false | Keep export disabled | Convert any nonempty string to true |
| seat_count:5 | Require a nonnegative integer; compare requested seats against5 | Treat5 as an unlimited true flag |
| Missing/null | Use your documented restrictive default | Assume the highest plan includes it |
| String/object/list | Validate against your own feature schema | Accept arbitrary JSON as a numeric limit |
The backend supports typed JSON values stored for features; the current dashboard's plan editor primarily exposes boolean feature selection, and there is no documented public feature-management API in v1. Do not invent a numeric-value dashboard control or use unsupported SQL against the shared production database. For a numeric entitlement deployment, confirm your supported configuration workflow first.
## Read with each official SDK
The [complete configuration examples](/docs/v1/security/offline-mode#complete-sdk-configuration) are the single-source runnable examples for all four languages. They output safe summaries and never unlock a feature on an invalid result.
| SDK | Validity gate | Boolean / numeric access |
| --- | --- | --- |
| Python | result.valid | result.feature(name, False); result.feature_value(name, default); reject bool when expecting int |
| JavaScript | result.valid | result.feature(name, false); result.featureValue(name, default); Number.isSafeInteger for limits |
| PHP | result->valid | result->feature(name, false); result->featureValue(name, default); is_int for limits |
| C# | result.Valid | result.Feature(name, false); result.FeatureValue(name); JsonElement type and integer checks |
Current boolean helpers do not all share the same numeric truthiness behavior. For true boolean access, inspect an actual boolean feature; for counts, use the value helper and explicit type/range checks. The examples demonstrate pdf_export and a restrictive seat_count fallback. Missing seat_count returns 0, not an invented entitlement. Deactivation success is not an active license payload and must not unlock features.
## Version feature contracts
1. Write down the meaning, type and restrictive default of each key.
2. Keep existing keys stable while released clients depend on them.
3. Introduce a new key, such as export_format_v2, when the meaning or type changes.
4. Release clients that understand both versions under explicit rules.
5. Validate both old and new clients with representative signed payloads.
6. Remove the old feature only after your compatibility policy permits it.
Expected result: an old client never mistakes a new numeric/object value for unrestricted access. The platform does not implement an automatic feature-schema negotiation service.
## Changes to a customer's plan
The signed response is built from the license's current plan and its stored feature rows. An already cached response retains its old feature snapshot until online validation refreshes it or its time policy refuses it. If an approved supported configuration workflow changes those rows, affected licenses observe the change on their next successful online check. The current dashboard offers plan creation and state/archive actions, not a general feature/plan editor; do not infer a public edit workflow from the engine's database reads.
There is no general public v1 endpoint that changes a license's plan. Use only an actually supported dashboard workflow; do not present renew as a plan-change operation. Renewal changes expiry, not feature schema. Where a transfer is not supported, plan an explicitly authorized reissue/migration and manage old [activations](/docs/v1/start/glossary#activation); do not silently run database edits.
## Test your gates
1. Run each configuration sample against a license with pdf_export enabled.
2. Validate online and confirm valid=true and pdf_export=true.
3. Use a license with that feature disabled and confirm false.
4. Test an expired/refused result that still contains feature data; confirm false.
5. Test missing, negative, string and boolean seat_count values; confirm the restrictive0 fallback.
6. Test an integer limit 5 and refuse a sixth seat.
Expected result: validity and type checks both participate in the decision. Numeric server configuration remains a separate capability from demonstrating a safe reader.
## Expected result
Valid active results permit only correctly typed entitlements. Cached feature changes follow signed validation/grace deadlines.
## Common mistakes
- Gating on the display plan name.
- Calling a helper without checking valid.
- Coercing strings, booleans or null into numeric limits.
## Security notes
Feature data is not a substitute for server-side authorization of valuable remote actions. A patched local client can bypass local gates; keep important online operations behind your own authenticated backend.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Feature change is not visible | Client is using an earlier signed cache | Validate online |
| Numeric feature unlocks everything | Boolean helper used for a count | Use an explicit integer comparison |
| Missing feature becomes enabled | Permissive default/coercion | Use a restrictive typed default |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in Sandbox.
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Set up your Sandbox
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/sandbox
---
slug: "/docs/v1/integration-guide/sandbox"
title: "Set up your Sandbox"
description: "Stage 2: set up your sandbox with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 17
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Create a verified evaluation workspace, without real [customer](/docs/v1/start/glossary#customer) payments.
1. Open [registration](/register) and select Free for [Sandbox](/docs/v1/start/glossary#sandbox) evaluation.
2. Enter an email you control and verify the code or single-use link from the message.
3. Complete your name, workspace name, password and terms acknowledgement.
4. In the workspace header, select **Sandbox**.
A workspace is the private area that contains your [products](/docs/v1/start/glossary#product) and [licenses](/docs/v1/start/glossary#license). Sandbox means test records, separate from [Live](/docs/v1/start/glossary#live). A paid platform selection can require checkout; it is not a customer license purchase.
## Expected result
You can open the dashboard and the environment reads Sandbox.
## If it does not work
Check spam and wait for the resend control's cooldown. Do not repeatedly submit new registrations. If email proof fails, contact support. A Live record will not appear in Sandbox.
## Stage checklist
- My inbox is verified and I can sign in.
- Sandbox is selected.
## Next steps
[Continue to Stage 3: Create your product and plan](/docs/v1/integration-guide/model)
# Create your product and plan
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/model
---
slug: "/docs/v1/integration-guide/model"
title: "Create your product and plan"
description: "Stage 3: create your product and plan with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 18
audience: "all"
level: "beginner"
last_updated: "2026-10-21"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Create one software [product](/docs/v1/start/glossary#product) and a worked example plan. If you already sell in WooCommerce, Stage 12 offers a combined import instead; the manual example below is still useful for learning.
1. Open **Products → New product** (or **Create product** on an empty page).
2. Enter **Product name** Quickstart App, **Slug** quickstart-app, **Platform** Desktop, and **[License](/docs/v1/start/glossary#license) prefix** QUICK. Choose **Create product**.
3. Saving opens the first-plan form in **Plans & [features](/docs/v1/start/glossary#feature)**. If it is closed, choose **New plan**.
4. Select Quickstart App. Enter **Plan name** Quickstart plan and **Slug** quickstart-plan.
5. Choose **Currency** EUR, **Price** 19.50, **Billing type** Monthly, **Duration days** 30, **Max [devices](/docs/v1/start/glossary#device)** 2 and **Feature keys** pdf_export.
6. Choose **Create plan**. Return to Products and copy the product's public **App ID**.
A product represents your software. Its App ID is the application identifier; there is no separate Application record to create. A plan describes access for that product. These plan prices are reference metadata, not a payment or recurring charge.
:::warning Use the demonstrated test values
The sample price and name are test data. Python SDK 1.1.5 fixes the earlier Python canonicalization defects; legacy JavaScript/C# limitations remain documented. Test your real payloads and never weaken verification. A production assessment is required at Stage 14.
:::
For the general model, Pro / two devices / one year means the same product with a plan whose duration is 365 days. Duration is not a payment instruction; your sales system handles payments.
## Expected result
The product card has an App ID and an active plan.
## If it does not work
A duplicate slug needs a new unique slug. A grey WooCommerce product needs an active plan or the combined product/plan import. Free limits are enforced; do not create more test products than your evaluation allows.
## Stage checklist
- I copied my [public App ID](/docs/v1/start/glossary#public-app-id).
- My plan enables pdf_export and two devices.
## Next steps
[Continue to Stage 4: Create a test license](/docs/v1/integration-guide/license-and-credentials)
# Threat model
Canonical: https://licensing.kdpika.com/docs/v1/security/threat-model
---
title: "Threat model"
description: "Understand licensing trust boundaries, attacker capabilities, implemented controls and limits on customer-controlled machines."
slug: "/docs/v1/security/threat-model"
section: "Security"
order: 18
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Use licensing as one layer of authorization with explicit trust boundaries, not as a promise that a local application cannot be patched.
## What you will learn
- Identify the assets and attacker capabilities.
- Map implemented controls to realistic threats.
- Decide which operations need server-side enforcement.
## Prerequisites
- Read [signed responses](/docs/v1/security/signed-responses).
- Know whether your app runs on your server or a [customer](/docs/v1/start/glossary#customer)'s [device](/docs/v1/start/glossary#device).
## Assets and trust boundaries
Protect [developer API keys](/docs/v1/start/glossary#developer-api-key), signing private keys, [activation tokens](/docs/v1/start/glossary#activation-token), [license](/docs/v1/start/glossary#license) state, customer data and your application's valuable operations. [Public App IDs](/docs/v1/start/glossary#public-app-id) and [public verification keys](/docs/v1/start/glossary#public-verification-key) are not secrets. A public key must still be trusted for the intended origin.
Your backend is a different trust boundary from a customer's desktop, plugin host, browser or CI machine. Keep lmp_ management credentials on your backend only. Neither obfuscation nor an environment variable makes a distributed management key private.
| Attacker | Attempt | Existing control | Limit / integration responsibility |
| --- | --- | --- | --- |
| Network attacker | Edit license [features](/docs/v1/start/glossary#feature) in transit | HTTPS and Ed25519 payload signatures | Maintain TLS validation and correct trusted-key handling |
| Customer with a copied token | Use another installation's [activation](/docs/v1/start/glossary#activation) | Token binding and signed installation hash | Persist/send the [installation ID](/docs/v1/start/glossary#installation-id); a fully copied identity/cache still needs your local defense |
| Unauthenticated caller | Guess [license keys](/docs/v1/start/glossary#license-key) or flood activation | High-entropy credentials, validation and existing limiters | Rate limiting is not an identity proof or complete DDoS defense |
| Another tenant | Read or change your license | [Organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment-scoped queries and permissions | No formal independent tenant-isolation audit is claimed |
| Leaked [developer](/docs/v1/start/glossary#developer) key holder | Perform permitted management operations | Scoped, product/environment-bound hashed keys; revocation/rotation | A valid stolen key works within its [scope](/docs/v1/start/glossary#scope) until revoked |
| Local administrator | Patch the executable or clock | Signature checks, cache deadlines, rollback checkpoints | Local enforcement can be patched; no absolute anti-crack guarantee |
| Signing-key thief | Forge trusted snapshots | Private key retained on server, not shipped in clients | Compromise requires coordinated key rollout; no automatic historical key ring |
| Replayer | Reuse an old valid response | Signed expiry/grace, binding and local clock checks | No signed nonce/challenge; disconnected state may be stale |
| Forged [webhook](/docs/v1/start/glossary#webhook) sender | Trigger provisioning changes | Endpoint HMAC and receiver validation | Receiver must enforce timestamp/deduplication and its own business authorization |
## Design decisions
1. List each paid operation and the asset it exposes.
2. Decide whether its final authorization can run on your trusted server.
3. For local-only use, choose a bounded offline policy and explain availability limits.
4. Keep management and signing secrets out of client artifacts and logs.
5. Test negative binding, time, signature and feature cases.
6. Document recovery for lost identity, expired grace and suspected compromise.
Expected result: every important action has an owner and a trust boundary, not merely a hidden button.
## What KDPika does not prove
A license signature is not encryption, proof of buyer payment, identity verification of a person, proof of an unmodified binary, a hardware attestation or a guarantee against replay. Licensing does not process payments from your software buyers. Your store/backend remains responsible for verified purchase fulfillment and refunds.
The available privacy/export/audit mechanisms are useful controls, not a certification. This source review is not a penetration test, independent security audit or claim of immunity from attacks.
## Expected result
A design review identifies which risks are reduced and which remain on your application, backend and operational process.
## Common mistakes
- Embedding a developer key in a desktop client.
- Claiming signatures stop a local administrator from changing code.
- Using a webhook or checkout success URL as license authorization.
## Security notes
Minimize personal data in payloads and logs. Keep server backups and key access restricted. If a suspected issue involves KDPika, report it through [the disclosure policy](/security) without testing other tenants.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Paid online action bypassed | Authorization enforced only in the UI | Enforce it again on your server |
| Offline revocation is delayed | Disconnected signed snapshot | Reduce grace or require online authorization |
| Security claim lacks evidence | Assumption treated as guarantee | State the limit and collect actual evidence |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Application hardening
Canonical: https://licensing.kdpika.com/docs/v1/security/hardening
---
title: "Application hardening"
description: "Combine realistic code signing, short validation policies, server authorization and tamper-evident operational records."
slug: "/docs/v1/security/hardening"
section: "Security"
order: 19
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Reduce abuse by protecting distribution and moving valuable authorization to trusted servers, without promising an unpatchable client.
## What you will learn
- Choose practical client and server defenses.
- Understand signing, obfuscation and logging limits.
- Apply platform-specific release checks.
## Prerequisites
- Review [the threat model](/docs/v1/security/threat-model).
- Implement [signature verification](/docs/v1/security/verify-ed25519) and [typed feature gates](/docs/v1/security/entitlements-and-features).
## Prioritized controls
1. Remove [developer](/docs/v1/start/glossary#developer) keys and signing private keys from all distributed artifacts.
2. Verify signed [license](/docs/v1/start/glossary#license) data and all [product](/docs/v1/start/glossary#product)/environment/installation/time bindings.
3. Gate each feature on a valid result and an expected entitlement type.
4. Protect tokens with the user's native secure storage where available.
5. Sign releases and verify updates before installing them.
6. Authorize valuable remote operations on your own authenticated backend.
7. Choose short validation/grace policies for high-value connected functions.
8. Review suspicious [request IDs](/docs/v1/start/glossary#request-id), [activation](/docs/v1/start/glossary#activation) patterns and audit events.
Expected result: controls protect distinct boundaries; no single local check is the sole authorization for a valuable server operation.
## Code signing is distribution integrity
| Platform | Recommended release practice | Important limit |
| --- | --- | --- |
| Windows | Authenticode-sign installers/executables using your publisher certificate; verify update signatures and protect the signing key | A valid publisher signature does not authorize a license or make a binary unpatchable |
| macOS | Use a consistent Developer ID signature, hardened runtime as applicable and notarization for direct distribution | Notarization does not validate your business-license logic |
| Linux | Use signed distribution packages/repositories or a verified signed release manifest appropriate to your distribution | There is no universal Linux code-signing mechanism enforced for every arbitrary executable |
[Apple's Developer ID guidance](https://developer.apple.com/developer-id/) describes direct-distribution signing and notarization. Use [Microsoft SignTool documentation](https://learn.microsoft.com/en-us/windows/win32/seccrypto/signtool) for Windows release signing. Signing-key custody, certificate renewal and recovery are your release responsibilities. These docs do not claim your application has already been signed or notarized.
## Obfuscation and anti-tamper
Obfuscation can raise analysis cost but is not a secure place for management secrets. Local integrity checks, debugger detection and encrypted strings can be removed by a capable attacker. Avoid invasive hardware fingerprinting or punitive behavior that locks out legitimate users.
Prefer several narrow gates near valuable operations, clear failure UX and reviewable server decisions. Do not disable TLS or signature checks because a packager changes paths or dependencies. Test packaged artifacts separately when you actually distribute them.
## Server-side authorization
Authenticate your application user and authorize each valuable API action on the server. Validate the relevant license state there with protected credentials and correctly scoped tenant mapping. Do not trust browser flags, a caller's claimed email, local feature booleans or a checkout success URL.
Shorter product intervals reduce cache staleness but increase API traffic and availability dependence. A 1-hour interval with 24 hours grace is an integration recommendation, not a guarantee of instant revocation. Strictly online functions need an explicit accepted-online requirement; force-online alone still permits SDK transport fallback.
## Tamper-evident records
KDPika audit logs record actions; the source does not establish an immutable external ledger or signed append-only audit chain. Do not describe ordinary database logs as tamper-proof.
For your own sensitive backend decisions, collect a minimal event ID, request_id, operation, outcome and UTC time. To detect edits, chain records with a server-held HMAC key and periodically anchor a digest to separately restricted storage. A chain alone cannot detect truncation without an independently retained checkpoint. Never store that HMAC key in the client or in the same unrestricted log directory. This is a recommended architecture, not an implemented KDPika feature.
## Release review
1. Scan the final package for secrets and debug dumps.
2. Verify the package/update signature on a clean user profile.
3. Test altered license payloads and copied installation state.
4. Test revoked, expired, wrong-environment and connection-failure states.
5. Confirm protected functions remain denied on refusal.
6. Confirm a legitimate user can reconnect, deactivate and obtain support.
Expected result: failures are understandable, recoverable and restrictive without destroying user data.
## Expected result
Your distribution, local checks and server operations have separate defenses with tested refusal and recovery paths.
## Common mistakes
- Equating obfuscation with secret storage.
- Relying on one UI button to enforce payment or entitlement.
- Calling a mutable database audit log tamper-proof.
## Security notes
Do not attempt destructive anti-tamper responses. Protect the user and their files even when a license is refused. Apply least privilege to update/signing credentials and monitor their use.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Update causes vault prompts | Publisher identity/signature changed | Preserve a consistent signing identity and test updates |
| Grace defeats online-only policy | SDK fallback was mistaken for fail-closed | Require an accepted online result on your server |
| Log chain seems intact after truncation | No separate checkpoint | Retain/verify an independently restricted anchor |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Create a test license
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/license-and-credentials
---
slug: "/docs/v1/integration-guide/license-and-credentials"
title: "Create a test license"
description: "Stage 4: create a test license with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 19
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Issue a [customer](/docs/v1/start/glossary#customer) key and distinguish public identifiers from secrets.
1. Open **[Licenses](/docs/v1/start/glossary#license)** while the header still says **[Sandbox](/docs/v1/start/glossary#sandbox)**.
2. Choose **New license** or **Create first license**.
3. Select Quickstart App / Quickstart plan in **[Product](/docs/v1/start/glossary#product) and plan**.
4. Enter your test **Customer email** and a simple **Customer name**, such as Quickstart Tester. Company and External customer ID are optional.
5. Choose **Create license**. Copy **Your new [license key](/docs/v1/start/glossary#license-key)**, shown once, into your private test configuration.
The dashboard creates or reuses the customer for this email. A new license has no activated [devices](/docs/v1/start/glossary#device) yet.
:::security Credential basics
The [public App ID](/docs/v1/start/glossary#public-app-id) identifies the software. A customer license key activates one purchase. The SDK receives a private [activation token](/docs/v1/start/glossary#activation-token) and stores an installation identity. A [developer API key](/docs/v1/start/glossary#developer-api-key) can manage licenses: create one under **API keys** only for a trusted sales backend, never for this client. If you supply an email during [activation](/docs/v1/start/glossary#activation), it must match the license.
:::
## Expected result
The new Sandbox license is active and has zero [active devices](/docs/v1/start/glossary#active-devices).
## If it does not work
If the key was lost, masked listings cannot reveal it. Issue a replacement using your normal support procedure, without sharing customer keys in feedback.
## Stage checklist
- I stored the full key privately.
- I have not put a developer API key in my app.
## Next steps
[Continue to Stage 5: Choose your language and app type](/docs/v1/integration-guide/choose)
# Choose your language and app type
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/choose
---
slug: "/docs/v1/integration-guide/choose"
title: "Choose your language and app type"
description: "Stage 5: choose your language and app type with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 20
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Choose one programming language and one place where access is enforced.
Choose Desktop app, Web / SaaS backend, CLI tool, Plugin / extension, or manual issuance. Select your language at [Choose your path](/docs/v1/start/choose-your-path). The guide supports four official SDKs and ten REST client routes.
| App type | Where the check belongs |
|---|---|
| Desktop | Inside the app; valuable online operations also need server authorization. |
| Web / SaaS backend | On your authenticated server, before protected work. |
| CLI / [developer](/docs/v1/start/glossary#developer) tool | In the tool process, with private per-user state. |
| Plugin / extension | In its trusted process; a browser extension needs a backend for valuable server actions. |
| Just issue keys | Use manual issuance or a verified store route in Stage 12; no SDK is required to send a key. |
Four [official SDKs](/docs/v1/start/glossary#official-sdk) are distinct from ten REST projects. JavaScript here means Node.js or Electron main, not a browser SDK. Unlisted languages use HTTPS/JSON and a conforming signature verifier.
## Expected result
Your chosen language follows you through code examples and links.
## If it does not work
Language and app-type choices organize examples; they do not create unsupported framework integrations. No browser-only check protects a SaaS backend.
## Stage checklist
- I know where my protected action runs.
- I chose an official SDK or accepted the extra REST verification work.
## Next steps
[Continue to Stage 6: Install and configure](/docs/v1/integration-guide/install)
# Secure token storage
Canonical: https://licensing.kdpika.com/docs/v1/security/secure-token-storage
---
title: "Secure token storage"
description: "Choose native token storage by operating system and language, with explicit SDK fallbacks and tested helper examples."
slug: "/docs/v1/security/secure-token-storage"
section: "Security"
order: 20
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Store [activation](/docs/v1/start/glossary#activation) credentials privately and fail clearly when a required native vault is unavailable.
## What you will learn
- Compare actual SDK storage with native-vault recommendations.
- Use complete macOS, Windows, Linux and Electron helper examples.
- Understand language bridges, mobile keystores and testing limits.
## Prerequisites
- Read [authentication](/docs/v1/start/authentication) and [the threat model](/docs/v1/security/threat-model).
- Use [the source archive](/docs/downloads/security-verifiers.zip); helpers are in security/storage.
- Test only generated disposable values, not a [customer](/docs/v1/start/glossary#customer)'s token.
## What the SDKs actually do
| SDK | Current token storage | Important limitation |
| --- | --- | --- |
| Python1.0.0 | Optional positive-priority keyring backend; permission-protected file fallback | prefer_keyring=true is a preference, not a fail-closed native-vault requirement; inspect storage_security |
| PHP1.0.0 | Namespaced files;0700 directory /0600 files where supported | No automatic Keychain, DPAPI or Secret Service |
| JavaScript1.0.0 | Namespaced permission-protected files in Node/Electron main process | No built-in Electron safeStorage adapter |
| C#1.0.0 | Per-user files, atomic replacement and Unix modes where supported | No automatic Windows Credential Manager/DPAPI or MAUI secure storage |
These helpers do not override unsupported SDK storage hooks. PHP, JavaScript and C# have no public token-store injection option. Do not encrypt a second copy while leaving the SDK's original plaintext credential and call that secure integration. A required native-vault SDK adapter needs a separately reviewed [product](/docs/v1/start/glossary#product) change. A REST client can call a helper at its own token persistence boundary.
## Choose a native backend
| OS / runtime | Recommended protection | Deployment behavior |
| --- | --- | --- |
| Windows desktop | Credential Manager or DPAPI CurrentUser, plus user-only file ACLs | DPAPI ciphertext belongs to that Windows user context; do not assume roaming/reinstall recovery |
| macOS desktop | Generic-password Keychain item scoped to your signed application/service | Vault may lock or ask for permission; retain consistent publisher identity |
| Linux desktop | Secret Service via libsecret or a verified desktop vault | Headless sessions may have no unlocked service; do not silently use plaintext |
| Electron | safeStorage in the main process after app readiness | Reject Linux basic_text; avoid exposing raw credentials over renderer IPC |
| Backend/CI | Restricted secret manager or private filesystem under a dedicated service account | Interactive desktop keychains may not exist |
| Android | Android Keystore-backed encryption for the token file | A token is arbitrary data; encrypt it with a protected key rather than claiming Keystore stores any string directly |
| iOS/iPadOS | Keychain with the appropriate accessibility/access group | Review device-only, backup, lock-state and reinstall behavior for your app |
The [Microsoft data-protection guide](https://learn.microsoft.com/en-us/dotnet/standard/security/how-to-use-data-protection) explains user/computer [scope](/docs/v1/start/glossary#scope). [Apple Keychain services](https://developer.apple.com/documentation/security/keychain-services) and [libsecret](https://gnome.pages.gitlab.gnome.org/libsecret/index.html) describe their native services. A local administrator or compromised same-user process remains a meaningful threat; native storage is not remote attestation.
## Language integration matrix
Use native APIs or an audited binding. The helper CLI contract is put/get/delete; send secret bytes through stdin, never a shell command string or argv. Capture get output in memory, never print it in a support log.
| Language | Windows | macOS | Linux / relevant alternative |
| --- | --- | --- | --- |
| Python | Reviewed keyring backend or DPAPI bridge | Reviewed keyring backend or Keychain helper | keyring/libsecret; explicitly inspect fallback |
| PHP | Server secret store; DPAPI helper for a desktop host | Keychain helper through proc_open with stdin | libsecret helper through proc_open; not browser storage |
| JavaScript | Electron safeStorage / native helper | Electron safeStorage / native helper | safeStorage with non-basic backend; no renderer secret |
| TypeScript | Typed wrapper over the JS/native boundary | Typed Keychain/helper wrapper | Typed Secret Service/helper wrapper |
| C# | CurrentUser DPAPI or Credential Manager | Native Keychain bridge | Secret Service binding/helper; MAUI SecureStorage on supported mobile |
| Java | Reviewed JNA/native Credential Manager binding | Native Keychain binding/helper | D-Bus/libsecret binding/helper; Android Keystore when mobile |
| Kotlin | JVM native binding on desktop | JVM Keychain bridge | JVM Secret Service; Android Keystore on Android |
| Swift | Native helper/Windows crypto boundary requires a port | Security framework Keychain | Reviewed Secret Service bridge; CryptoKit sample alone does not store tokens |
| Go | Native syscall/binding for Credential Manager/DPAPI | Native Keychain bridge | D-Bus Secret Service binding/helper |
| Rust | Audited OS-vault crate with pinned backend [features](/docs/v1/start/glossary#feature) | Keychain-native backend | Secret Service backend; verify that no plaintext fallback is selected |
| Ruby | FFI/native helper with stdin | Keychain helper with stdin | Secret Service helper with stdin |
| Dart | Platform channel to native vault | Platform channel to Keychain | Desktop Secret Service plugin/channel; mobile keystore-backed secure storage |
| C | WinCred / CryptProtectData | SecItemAdd/CopyMatching/Update/Delete | libsecret C API |
| C++ | RAII wrapper around Windows APIs | RAII wrapper around Security APIs | RAII wrapper around libsecret |
These are architecture choices, not claims that every third-party language binding was installed or audited. Pin and review the binding you select; test its exact OS and lock/fallback behavior.
## macOS complete helper
Build with swiftc storage/keychain.swift -o keychain. Run keychain put ITEM with the generated test secret on stdin, keychain get ITEM with stdout captured privately, and keychain delete ITEM. The service namespace is reserved for this documentation example; use your own reviewed service identity in an application.
```swift
// macOS Keychain helper. Swift6/macOS13+, Foundation/Security. No dependency.
// Run: keychain put|get|delete ITEM; put reads secret from stdin, get writes stdout.
// Expected exit0 on success; generic error on failure. Never pass tokens in argv.
import Foundation
import Security
let service = "com.kdpika.documentation.storage-example"
func query(_ item: String) -> [String: Any] {
[kSecClass as String:kSecClassGenericPassword, kSecAttrService as String:service,
kSecAttrAccount as String:item]
}
do {
guard CommandLine.arguments.count == 3 else { throw NSError(domain: "Usage",code:1) }
let action = CommandLine.arguments[1], item = CommandLine.arguments[2]
guard item.range(of: #"^[A-Za-z0-9._-]{8,128}$"#,options:.regularExpression) != nil else { throw NSError(domain:"Item",code:1) }
let base = query(item); var status: OSStatus
switch action {
case "put":
let data = FileHandle.standardInput.readData(ofLength: 16385)
guard !data.isEmpty && data.count <= 16384 else { throw NSError(domain:"Size",code:1) }
status = SecItemUpdate(base as CFDictionary,[kSecValueData as String:data] as CFDictionary)
if status == errSecItemNotFound {
var add = base; add[kSecValueData as String] = data
add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
status = SecItemAdd(add as CFDictionary,nil)
}
case "get":
var request = base; request[kSecReturnData as String] = true; request[kSecMatchLimit as String] = kSecMatchLimitOne
var result: CFTypeRef?; status = SecItemCopyMatching(request as CFDictionary,&result)
if status == errSecSuccess, let data = result as? Data { FileHandle.standardOutput.write(data) }
case "delete":
status = SecItemDelete(base as CFDictionary)
if status == errSecItemNotFound { status = errSecSuccess }
default: throw NSError(domain:"Usage",code:1)
}
guard status == errSecSuccess else { throw NSError(domain:"Keychain",code:Int(status)) }
} catch {
FileHandle.standardError.write(Data("Secure storage is unavailable.\n".utf8)); exit(1)
}
```
## Windows complete helper
Build dotnet build storage/Dpapi.csproj -o build/dpapi; run dotnet build/dpapi/Dpapi.dll put, get or delete. It uses a fixed per-user example directory and CurrentUser protection. Set appropriate Windows ACLs on the application directory; chmod is not a Windows ACL policy.
```xml
Exenet7.0enableenable
```
```csharp
// Windows CurrentUser DPAPI helper. .NET7+, ProtectedData7.0.0; Windows only.
// Run built Dpapi.dll put|get|delete. put reads stdin; get writes raw secret stdout.
// Exit0 on success; unavailable/error exits1. This is not an SDK storage adapter.
using System.Security.Cryptography;
try {
if (!OperatingSystem.IsWindows() || args.Length != 1) throw new PlatformNotSupportedException();
var directory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"KDPikaSecureStorageExample");
var path = Path.Combine(directory,"activation-token.dpapi");
switch (args[0]) {
case "put":
using (var input = Console.OpenStandardInput()) {
var clear = new byte[16385]; var count = 0;
while (count < clear.Length) { var n = input.Read(clear,count,clear.Length-count); if(n==0) break; count+=n; }
if (count == 0 || count > 16384) throw new ArgumentException();
var protectedBytes = ProtectedData.Protect(clear.AsSpan(0,count).ToArray(),null,DataProtectionScope.CurrentUser);
CryptographicOperations.ZeroMemory(clear);
Directory.CreateDirectory(directory);
var temporary = path + "." + Guid.NewGuid().ToString("N");
File.WriteAllBytes(temporary,protectedBytes); File.Move(temporary,path,true);
}
break;
case "get":
var secret = ProtectedData.Unprotect(File.ReadAllBytes(path),null,DataProtectionScope.CurrentUser);
using(var output=Console.OpenStandardOutput()) output.Write(secret);
CryptographicOperations.ZeroMemory(secret); break;
case "delete": File.Delete(path); break;
default: throw new ArgumentException();
}
return 0;
} catch { Console.Error.WriteLine("Secure storage is unavailable."); return 1; }
```
## Linux complete helper
Install libsecret's secret-tool and an unlocked Secret Service implementation. Run python3 storage/secret_service.py put ITEM, get ITEM or delete ITEM. The helper has no plaintext fallback.
```python
"""Linux Secret Service helper. Python3.9+, libsecret secret-tool, unlocked vault.
Run: python3 secret_service.py put|get|delete ITEM. put reads stdin; get writes stdout.
Exit0 on success; no plaintext fallback. Never pass the token in an argument.
"""
import re
import subprocess
import sys
def operate(action, item, value=None):
if not re.fullmatch(r'[A-Za-z0-9._-]{8,128}', item):
raise ValueError('Invalid item')
attributes = ['application', 'com.kdpika.documentation.storage-example', 'installation', item]
if action == 'put':
if not isinstance(value, bytes) or not 0 < len(value) <= 16384:
raise ValueError('Invalid secret size')
command = ['secret-tool', 'store', '--label=KDPika activation token'] + attributes
elif action == 'get': command = ['secret-tool', 'lookup'] + attributes
elif action == 'delete': command = ['secret-tool', 'clear'] + attributes
else: raise ValueError('Unknown action')
result = subprocess.run(command, input=value if action == 'put' else None,
capture_output=True, timeout=30, check=True)
return result.stdout.rstrip(b'\n') if action == 'get' else b''
if __name__ == '__main__':
try:
if len(sys.argv) != 3: raise ValueError('Expected action and item')
action, item = sys.argv[1:]
value = sys.stdin.buffer.read(16385) if action == 'put' else None
sys.stdout.buffer.write(operate(action,item,value))
except Exception:
print('Secure storage is unavailable.',file=sys.stderr)
sys.exit(1)
```
## Electron complete helper
Import tokenStore into the Electron main process, after app.whenReady(). Pass Electron's safeStorage and a private per-user directory, then call put(token), get() or remove(). Do not accept arbitrary file paths from an untrusted renderer.
```javascript
// Electron main-process safeStorage helper. Electron>=28, Node18+, built-in fs.
// Pass Electron's safeStorage after app.whenReady(), then put/get/remove.
// Reject unavailable encryption and Linux basic_text; never plaintext fallback.
import fs from 'node:fs';
import path from 'node:path';
import crypto from 'node:crypto';
export function tokenStore(safeStorage, directory, platform = process.platform) {
const file = path.join(directory,'activation-token.encrypted');
function requireVault() {
if (!safeStorage.isEncryptionAvailable() ||
(platform === 'linux' && (!safeStorage.getSelectedStorageBackend || safeStorage.getSelectedStorageBackend() === 'basic_text'))) {
throw new Error('Secure storage is unavailable.');
}
}
return {
put(token) {
requireVault(); if (typeof token !== 'string' || !token || Buffer.byteLength(token)>16384) throw Error('Invalid token size');
fs.mkdirSync(directory,{recursive:true,mode:0o700});
const encrypted=safeStorage.encryptString(token), temporary=file+'.'+crypto.randomUUID();
fs.writeFileSync(temporary,encrypted,{mode:0o600,flag:'wx'}); fs.renameSync(temporary,file);
},
get() { requireVault(); return safeStorage.decryptString(fs.readFileSync(file)); },
remove() { if(fs.existsSync(file)) fs.unlinkSync(file); }
};
}
```
The sample uses the established synchronous safeStorage API for compatibility; it can block on vault prompts. Current Electron also offers an asynchronous API, which should be evaluated with its availability/rotation semantics when you adopt a specific newer runtime. See [Electron's storage security notes](https://www.electronjs.org/docs/latest/api/safe-storage). Never enable its plaintext-encryption fallback to claim native protection.
## Verify and recover
1. Generate a random disposable test value in memory.
2. Store it in a unique test item/directory.
3. Read it back into memory and compare exact bytes.
4. Confirm a missing/locked vault returns a clear error and never writes a plaintext fallback.
5. Delete only the uniquely named test item.
6. Confirm a later read fails.
Expected result: round-trip succeeds only under the intended user/vault, and cleanup affects no real token. macOS Keychain execution, Windows compilation/refusal on macOS, Linux subprocess contract and mocked Electron fallback/refusal are recorded separately. Native Windows/Linux and real Electron vault execution are not claimed.
## Expected result
A chosen storage boundary protects the actual token copy, has a visible fallback policy and handles locked/missing vaults without granting access.
## Common mistakes
- Storing credentials in browser localStorage or shipping a [developer](/docs/v1/start/glossary#developer) key.
- Assuming prefer_keyring guarantees native encryption.
- Keeping both an encrypted helper copy and an SDK plaintext copy unnoticed.
## Security notes
Never log tokens, put them in command-line arguments or expose them to renderer JavaScript. Restrict cache/identity files too: they can contain personal data and influence trust. Store a public-key pin through a trusted deployment policy.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Vault access fails | Locked keychain, unavailable service or denied access | Ask the user to unlock/allow the vault; fail clearly |
| Linux reports basic_text | No usable desktop secret backend | Refuse native-storage claims and configure a real vault |
| Token vanished after reinstall | User profile/vault changed | Reactivate under the device-limit recovery policy |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Installation identity
Canonical: https://licensing.kdpika.com/docs/v1/security/installation-id
---
title: "Installation identity"
description: "Generate a random stable installation ID without hardware fingerprints and recover safely after reinstall or state loss."
slug: "/docs/v1/security/installation-id"
section: "Security"
order: 21
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Persist one random [installation ID](/docs/v1/start/glossary#installation-id) per application installation and reuse it for [activation](/docs/v1/start/glossary#activation) and validation.
## What you will learn
- Generate and retain a privacy-conscious identity.
- Understand server hashes, validation binding and [device](/docs/v1/start/glossary#device) limits.
- Recover from reinstall without silently consuming extra devices.
## Prerequisites
- Read [the activation flow](/docs/v1/start/activation-validation-deactivation).
- Keep state in a private per-user directory.
- Use the [official SDK](/docs/v1/start/glossary#official-sdk)'s existing identity unless you are implementing a REST client.
## Identity contract
Activation's device_id is a trimmed string16–255 characters. Validation sends the same value in X-Installation-ID. The platform stores a keyed HMAC-SHA256 device hash; the signed response includes plain SHA-256 of the installation ID so the client can compare its own identity without the server pepper.
The raw installation ID is not stored in the device row. Optional device name, platform and application version are separate descriptive fields. A hash associated with a [customer](/docs/v1/start/glossary#customer) is not automatically anonymous data.
Use cryptographically random UUIDs or similarly high-entropy random IDs. Do not use a MAC address, disk serial, CPU ID, advertising ID, email address, IP address or a cross-product hardware fingerprint. A random installation ID reduces unnecessary device tracking but does not remove your privacy obligations. [CNIL distinguishes pseudonymisation from anonymisation](https://www.cnil.fr/fr/node/698).
## Persist before activation
1. Select a private per-user directory namespaced by application, origin, [product](/docs/v1/start/glossary#product) and environment.
2. Create the random ID once, before the first activation request.
3. Save it without overwriting an existing identity.
4. Reuse it on every run and include it with validation.
5. Compare its SHA-256 to the verified signed device.installation_hash.
Expected result: restarting the same installation does not create a new identity. Serialize first-run initialization; parallel processes must not race a partially written file.
Official Python, PHP, JavaScript and C# clients create and persist their own identities. Do not generate a new custom ID beside the SDK and send inconsistent values.
## Complete REST identity example
Python3.9+, standard library. Run python3 installation.py /absolute/private/path/installation-id. Two runs print the same UUID. Use a new temporary directory for tests. The path is your explicit local file, not a public web directory.
```python
"""Random persisted installation identity, Python3.9+; no hardware fingerprint.
Run: python3 installation.py PATH_TO_PRIVATE_ID_FILE. Output the same UUID each run.
This standalone REST-client example does not replace an official SDK identity.
"""
import os
import pathlib
import sys
import uuid
def installation_id(path):
path = pathlib.Path(path)
path.parent.mkdir(mode=0o700,parents=True,exist_ok=True)
try:
value = str(uuid.uuid4())
with path.open('x') as file:
os.chmod(path,0o600)
file.write(value+'\n')
except FileExistsError:
pass
# A damaged identity is not silently replaced and charged as a new device.
value = path.read_text().strip()
if str(uuid.UUID(value)) != value: raise ValueError('Invalid stored identity')
return value
if __name__ == '__main__':
try:
if len(sys.argv)!=2: raise ValueError('Expected private identity path')
print(installation_id(sys.argv[1]))
except Exception:
print('Installation identity is unavailable. Restore it or contact support.',file=sys.stderr)
sys.exit(1)
```
A damaged identity is refused, not silently replaced. This demonstration uses exclusive file creation; use a startup lock if multiple processes initialize concurrently. Windows needs appropriate user-only ACLs, not an assumption that Unix modes are enforced.
## Device and activation limits
The server enforces concurrent active-device limits and any cumulative activation limit. Deactivation frees an active-device seat but does not decrement cumulative activation_count. Re-activation can rotate tokens; do not assume an old token remains valid.
A copied VM image containing the same ID/token/cache is not a genuinely new trusted installation merely because the hardware differs. Conversely, deleting the ID and generating a new one can consume a new seat or hit a cumulative limit. Do not reset identities as a retry strategy for DEVICE_LIMIT_REACHED.
## Reinstall and recovery
1. If the app still runs, deactivate the old installation before removing its state.
2. If reinstall retains the same trusted user state, reuse the existing ID.
3. If identity was lost, generate a new ID only through a clear reactivation path.
4. Explain DEVICE_LIMIT_REACHED without accusing the customer.
5. Ask the customer to release an old device or contact the [developer](/docs/v1/start/glossary#developer) for a supported reset.
6. After reset, activate again; old revoked tokens do not revive.
Expected result: recovery uses authorized device actions rather than fabricating more identifiers. Preserve user files even when a [license](/docs/v1/start/glossary#license) cannot be reactivated.
## Expected result
The same installation retains its ID and bound token across normal restarts; reinstall recovery respects server device limits.
## Common mistakes
- Regenerating an ID on every launch.
- Using a hardware serial or email as device_id.
- Omitting the validation installation header because it is transport-optional.
## Security notes
Keep IDs and token state private. Never treat a device hash as proof of a unique person or immutable physical device. Avoid broad backups that clone active credentials into new installations.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| DEVICE_MISMATCH | ID differs from the activated installation | Restore matching state or reactivate deliberately |
| DEVICE_LIMIT_REACHED after reinstall | Lost identity created another device | Release the old seat or request a supported reset |
| Invalid saved ID | Corruption or unexpected state modification | Refuse silently replacing it; restore or contact support |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Install and configure
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/install
---
slug: "/docs/v1/integration-guide/install"
title: "Install and configure"
description: "Stage 6: install and configure with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 21
audience: "all"
level: "beginner"
last_updated: "2026-10-21"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Install exactly the published source archive, then configure a private [Sandbox](/docs/v1/start/glossary#sandbox) example.
:::tabs
:::tab python Python
### Python setup
Use Python 3.11–3.14 and a working HTTPS certificate store. The official package is published on [PyPI](https://pypi.org/project/kdpika-licensing/) with [GitHub source](https://github.com/kdpika/kdpika-licensing). Create a virtual environment, then install:
```bash
python3 -m venv .venv
.venv/bin/python -m pip install "kdpika-licensing==1.1.5"
.venv/bin/python -m pip check
```
On native Windows use py -m venv .venv, then .venv\\Scripts\\python.exe for both pip commands and the example. Import kdpika_licensing. Use the same virtualenv interpreter for pip and your example.
:::
:::tab javascript JavaScript / Node.js
### JavaScript setup
Requires Node.js 18+ source compatibility, npm and an authenticated backend, CLI or Electron main process. Do not run this SDK in a web page. Download [JavaScript SDK 1.0.0](/docs/downloads/kdpika-javascript-sdk-1.0.0.zip) and extract into kdpika-javascript-sdk, then run:
```bash
npm install --ignore-scripts --save-exact ./kdpika-javascript-sdk
```
Save your example with the .mjs extension. The installed module is @kdpika/license-sdk. Choose a maintained Node runtime for production and run the compatibility tests there.
:::
:::tab php PHP
### PHP setup
Requires PHP 8.2+, cURL, Sodium and Composer. The package's older ^8.1 declaration is not its actual minimum. Download [PHP SDK 1.0.0](/docs/downloads/kdpika-php-sdk-1.0.0.zip) and extract into kdpika-php-sdk:
```bash
composer install --working-dir ./kdpika-php-sdk --no-interaction --no-plugins --no-scripts --prefer-dist
```
The example loads that folder's vendor/autoload.php. It runs on your trusted backend or CLI, not in browser JavaScript. KDP_SDK_DIR can point to a different extracted package folder.
:::
:::tab csharp C# / .NET
### C# setup
Requires the .NET SDK and the unchanged SDK's net7.0 source target. Download [C# SDK 1.0.0](/docs/downloads/kdpika-csharp-sdk-1.0.0.zip) and extract into kdpika-csharp-sdk. Save the project below beside that folder:
```xml
Exenet7.0enableenablefalse
```
```bash
dotnet restore Guide.csproj
```
The demonstrated net7 target is an existing compatibility constraint, not a recommendation to deploy an end-of-support runtime. Production needs a maintained-runtime compatibility assessment; see the disclosed SDK limitations.
:::
:::tab curl cURL / other language
### cURL setup
This is a transport exercise, not an SDK or signature verifier. Use this route for a REST-only language, then follow its real client and verification project before granting access.
```bash
umask 077
curl --fail --silent --show-error -o response-private.json \
-H "X-App-ID: $KDP_APP_ID" -H "X-Environment: SANDBOX" \
-H "Content-Type: application/json" \
--data '{"license_key":"YOUR_SANDBOX_LICENSE_KEY","device_id":"guide-install-CHANGE-ONCE"}' \
https://licensing.kdpika.com/api/v1/licenses/activate
```
Replace the key privately. Keep the device_id stable for this installation. The file contains a private [activation token](/docs/v1/start/glossary#activation-token): do not publish or log it. HTTP success alone does not authorize your app. Read [activation](/docs/v1/api/activate), then choose your [REST client](/docs/v1/sdks/overview) and [signature verification](/docs/v1/security/verify-ed25519).
:::
:::
## Configure this terminal
Set these variables in your terminal. App ID is public; your [customer](/docs/v1/start/glossary#customer)'s key and state directory are private. Do not use a [developer API key](/docs/v1/start/glossary#developer-api-key) in this client.
:::tabs
:::tab bash macOS / Linux · Bash
```bash
export KDP_APP_ID="YOUR_PUBLIC_APP_ID"
export KDP_CUSTOMER_EMAIL="YOUR_TEST_CUSTOMER_EMAIL"
export KDP_STATE_DIR="$HOME/.kdpika-guide-sandbox"
read -r -s -p "Sandbox license key: " KDP_LICENSE_KEY
printf '\n'
export KDP_LICENSE_KEY
```
For a different [device](/docs/v1/start/glossary#device) test, use a new private state directory, not a different email.
:::
:::tab powershell Windows · PowerShell
```powershell
$env:KDP_APP_ID = "YOUR_PUBLIC_APP_ID"
$env:KDP_CUSTOMER_EMAIL = "YOUR_TEST_CUSTOMER_EMAIL"
$env:KDP_STATE_DIR = Join-Path $env:LOCALAPPDATA "KDPikaGuideSandbox"
$secureKey = Read-Host "Sandbox license key" -AsSecureString
$env:KDP_LICENSE_KEY = [System.Net.NetworkCredential]::new("", $secureKey).Password
```
The variable is plaintext in this process; do not print it or leave it in a shared terminal. Close the terminal after testing. Windows Python uses .venv\\Scripts\\python.exe; Node/PHP/.NET commands are the same if their runtimes are installed.
:::
:::
Expected result: the variables are available only to this terminal and its child processes. Use a user-owned directory, outside a web root or shared repository. The sample uses Sandbox explicitly.
## Expected result
Your chosen runtime can load the SDK and write private installation state. Python uses its verified PyPI release; other SDKs retain their documented source-install requirements.
## If it does not work
If extraction creates another directory level, point the install command at the folder containing pyproject.toml, package.json, composer.json or KDPika.LicenseSdk.csproj. PHP needs 8.2+ despite older metadata.
## Stage checklist
- I installed my chosen SDK from its source ZIP.
- I configured App ID, test email, Sandbox key and private state path.
## Next steps
[Continue to Stage 7: Activate your first license](/docs/v1/integration-guide/activate)
# API key security and leak response
Canonical: https://licensing.kdpika.com/docs/v1/security/api-key-leak-response
---
title: "API key security and leak response"
description: "Scope management credentials and respond to a leak with immediate revocation, request-ID investigation and controlled recovery."
slug: "/docs/v1/security/api-key-leak-response"
section: "Security"
order: 22
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Keep [developer API keys](/docs/v1/start/glossary#developer-api-key) on trusted servers and revoke an exposed key before relying on removal of the public copy.
## What you will learn
- Use the three registered management [scopes](/docs/v1/start/glossary#scope).
- Rotate without assuming a grace overlap.
- Investigate and recover from confirmed or suspected exposure.
## Prerequisites
- Read [authentication](/docs/v1/start/authentication).
- Have authorized access to your workspace's API keys, API logs and audit logs.
- Do not paste secrets into support messages or documentation feedback.
## Prevent exposure
Use one named key per service, [product](/docs/v1/start/glossary#product) and environment where practical. Store it in backend secret configuration and give its service account the minimum access. [Sandbox](/docs/v1/start/glossary#sandbox) and [Live](/docs/v1/start/glossary#live) credentials must not be interchangeable.
| Scope | Current public operations |
| --- | --- |
| [licenses](/docs/v1/start/glossary#license):read | Read a license by ID |
| licenses:write | Create, revoke, suspend, restore and renew licenses |
| [devices](/docs/v1/start/glossary#device):write | Reset devices for a license |
An arbitrary scope string does not add a permission. A read-only integration does not need licenses:write. A desktop app uses the [public App ID](/docs/v1/start/glossary#public-app-id) and installation token, never an lmp_ key.
Scan release packages, repository changes, CI logs, screenshots and crash reports. Do not put credentials in URLs, shell history or analytics. Redact Authorization and raw [customer](/docs/v1/start/glossary#customer) payloads from your own logs.
## Immediate containment
1. Identify the affected key in API keys by its display prefix, product, environment and name.
2. Revoke it immediately if exposure is suspected; do not wait for evidence of misuse.
3. If using Rotate, securely copy the new secret once and deploy it to the intended backend.
4. Confirm the old key no longer works and the replacement performs only its intended operation.
5. Remove the exposed copy from active configuration and distribution channels.
Expected result: the old credential cannot authenticate. Rotation revokes the old key immediately; there is no automatic overlap. Coordinate deployment or accept a short controlled interruption.
Do not test a leaked credential against tenants you do not own. Deleting a public message or commit does not revoke the credential and cannot guarantee nobody copied it.
## Investigation
1. Record the earliest possible exposure time and affected product/environment.
2. Review API logs for method, endpoint, HTTP status, time and request_id.
3. Correlate [request IDs](/docs/v1/start/glossary#request-id) and affected resource IDs with audit logs and your own backend logs.
4. Identify actual license creation, state changes or device resets during the window.
5. Preserve only necessary investigation records in restricted storage.
Expected result: distinguish confirmed actions from suspicions. The API log does not retain request bodies or Authorization tokens; missing payload details are not proof no incident occurred. Retention varies by plan and cleanup scheduling.
## Recover affected licenses
1. List only the licenses affected by confirmed unauthorized actions.
2. Suspend or revoke those requiring containment under your policy.
3. Review unintended device resets and ask legitimate customers to reactivate when necessary.
4. Reissue or restore access only after confirming the intended customer and product.
5. Validate the repaired state in Sandbox or an explicitly authorized real incident workflow.
6. Monitor new failures and unexpected management requests.
Expected result: recovery is targeted. Do not revoke every customer's license without evaluating impact. Revoked [activation tokens](/docs/v1/start/glossary#activation-token) do not automatically revive when a license is restored.
## Notify and improve
Inform affected parties with confirmed facts, impact, steps taken and support options. Avoid speculative accusations or disclosing other customers' data. Assess personal-data breach obligations with qualified advice; this playbook is not a legal determination.
Restrict new keys, repair the leaking pipeline, rotate other actually exposed credentials and record the incident. A management key, activation token, [webhook](/docs/v1/start/glossary#webhook) secret and signing private key are different credentials with different remediation. Suspected signing-key exposure must be escalated to [KDPika security contact](/security).
## Expected result
The leaked key is invalid, replacement scope is minimal, affected actions are identified and recovery is documented.
## Common mistakes
- Removing a public secret without revoking it.
- Assuming rotated credentials overlap safely.
- Claiming API logs contain bodies that the logger intentionally does not capture.
## Security notes
Keep the investigation private and redact credentials. Review operational access to server configuration and backups; cryptographic hashing at rest does not make an exposed usable token harmless.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Backend fails after rotation | Immediate old-key revocation | Deploy the new secret securely |
| No full request body in logs | Privacy-conscious metadata-only API logging | Correlate request_id with your own restricted logs |
| Customer cannot reactivate after restore | Old token remains revoked | Use a fresh authorized [activation](/docs/v1/start/glossary#activation) |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in Sandbox.
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Activate your first license
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/activate
---
slug: "/docs/v1/integration-guide/activate"
title: "Activate your first license"
description: "Stage 7: activate your first license with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 22
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Save one complete file and run it. It also supports later validate, check and deactivate actions. Keep this file through Stages 8–13.
:::tabs
:::tab python Python
Save **license-check.py** in your example folder.
```python
"""Sandbox learning example; use private state, never a developer API key."""
import os
import sys
from pathlib import Path
from kdpika_licensing import LicenseClient
try:
action = sys.argv[1] if len(sys.argv) == 2 else "activate"
if action not in ("activate", "validate", "check", "deactivate"):
raise ValueError("Choose an action")
client = LicenseClient(
product=os.environ["KDP_APP_ID"], environment="SANDBOX",
base_url=os.getenv("KDP_BASE_URL", "https://licensing.kdpika.com"),
storage_dir=Path(os.environ["KDP_STATE_DIR"]), prefer_keyring=False)
result = client.activate(os.environ["KDP_LICENSE_KEY"],
os.getenv("KDP_CUSTOMER_EMAIL") or None) if action == "activate" else getattr(client, action)()
released = action == "deactivate" and result.valid
allowed = action != "deactivate" and result.valid and result.signature_valid
print("Device released." if released else "Sandbox license verified." if allowed
else "License could not be confirmed. Check your key or contact support.")
sys.exit(0 if released or allowed else 1)
except Exception:
print("License check unavailable. Check configuration and connection.", file=sys.stderr)
sys.exit(2)
```
Run:
```bash
.venv/bin/python license-check.py activate
```
:::
:::tab javascript JavaScript / Node.js
Save **license-check.mjs** in your example folder.
```javascript
// Node.js / Electron main process, not browser JavaScript.
import { LicenseClient } from '@kdpika/license-sdk';
try {
const action = process.argv[2] || 'activate';
if (!['activate', 'validate', 'check', 'deactivate'].includes(action)) throw Error();
const client = new LicenseClient({
product: process.env.KDP_APP_ID, environment: 'SANDBOX',
baseUrl: process.env.KDP_BASE_URL || 'https://licensing.kdpika.com',
storageDir: process.env.KDP_STATE_DIR
});
if (!process.env.KDP_STATE_DIR) throw Error();
const result = action === 'activate'
? await client.activate(process.env.KDP_LICENSE_KEY, process.env.KDP_CUSTOMER_EMAIL || null)
: await client[action]();
const released = action === 'deactivate' && result.valid;
const allowed = action !== 'deactivate' && result.valid && result.signatureValid;
console.log(released ? 'Device released.' : allowed ? 'Sandbox license verified.'
: 'License could not be confirmed. Check your key or contact support.');
process.exitCode = released || allowed ? 0 : 1;
} catch {
console.error('License check unavailable. Check configuration and connection.');
process.exitCode = 2;
}
```
Run:
```bash
node license-check.mjs activate
```
:::
:::tab php PHP
Save **license-check.php** in your example folder.
```php
activate(getenv('KDP_LICENSE_KEY') ?: '', getenv('KDP_CUSTOMER_EMAIL') ?: null)
: $client->$action();
$released = $action === 'deactivate' && $result->valid;
$allowed = $action !== 'deactivate' && $result->valid && $result->signatureValid;
echo $released ? 'Device released.' : ($allowed ? 'Sandbox license verified.'
: 'License could not be confirmed. Check your key or contact support.'), PHP_EOL;
exit($released || $allowed ? 0 : 1);
} catch (Throwable) {
fwrite(STDERR, 'License check unavailable. Check configuration and connection.'.PHP_EOL);
exit(2);
}
```
Run:
```bash
php license-check.php activate
```
:::
:::tab csharp C# / .NET
Save **Program.cs** in your example folder.
```csharp
using KDPika.LicenseSdk;
static string Required(string name) => Environment.GetEnvironmentVariable(name)
is string value && value.Length > 0 ? value : throw new ArgumentException();
try {
var action = args.Length == 0 ? "activate" : args[0];
if (!new[]{"activate","validate","check","deactivate"}.Contains(action)) throw new ArgumentException();
var client = new LicenseClient(Required("KDP_APP_ID"), "SANDBOX",
Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com",
storageDir:Required("KDP_STATE_DIR"));
var result = action switch {
"activate" => await client.ActivateAsync(Required("KDP_LICENSE_KEY"),
Environment.GetEnvironmentVariable("KDP_CUSTOMER_EMAIL")),
"validate" => await client.ValidateAsync(), "check" => await client.CheckAsync(),
_ => await client.DeactivateAsync()
};
var released = action == "deactivate" && result.Valid;
var allowed = action != "deactivate" && result.Valid && result.SignatureValid;
Console.WriteLine(released ? "Device released." : allowed ? "Sandbox license verified."
: "License could not be confirmed. Check your key or contact support.");
Environment.ExitCode = released || allowed ? 0 : 1;
} catch {
Console.Error.WriteLine("License check unavailable. Check configuration and connection.");
Environment.ExitCode = 2;
}
```
Run:
```bash
dotnet run --project Guide.csproj -- activate
```
:::
:::tab curl cURL / REST clients
Use the cURL transport flow in the install stage, then the selected REST project's [activation](/docs/v1/start/glossary#activation) command. It is not an [official SDK](/docs/v1/start/glossary#official-sdk); do not grant access until signature and semantic checks pass. [Browse all REST projects](/docs/v1/sdks/overview).
:::
:::
## Expected result
For a valid fresh activation, standard output is exactly:
```text
Sandbox license verified.
```
The program exits with code 0. In the dashboard, open **[Devices](/docs/v1/start/glossary#device)** or the [license](/docs/v1/start/glossary#license)'s **Devices** count: one installation is active. The sample never prints a token or full key.
## If it does not work
| What you see | What to do |
|---|---|
| INVALID_LICENSE | Check the full key, [public App ID](/docs/v1/start/glossary#public-app-id), [Sandbox](/docs/v1/start/glossary#sandbox) selection and matching [customer](/docs/v1/start/glossary#customer) email. |
| MAX_DEVICES_REACHED | Deactivate an old installation or inspect Devices. Do not make random identities. |
| SIGNATURE_INVALID / cached signature error | Keep access locked. Check the runtime, clock and SDK compatibility notes; do not disable verification. |
| NOT_ACTIVATED | Run activate in the same private state directory first. |
| NETWORK_ERROR / no connection | Reconnect. A first activation needs internet; no cached response means no offline grant. |
| LICENSE_EXPIRED / LICENSE_REVOKED / LICENSE_SUSPENDED | Explain the state to the customer and provide support; never silently convert it to valid. |
## Stage checklist
- My chosen sample ran against Sandbox.
- I checked the active device in the dashboard.
- I did not unlock [features](/docs/v1/start/glossary#feature) on an unverified answer.
## Next steps
[Continue to Stage 8: Validate when your app starts](/docs/v1/integration-guide/validate)
# Validate when your app starts
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/validate
---
slug: "/docs/v1/integration-guide/validate"
title: "Validate when your app starts"
description: "Stage 8: validate when your app starts with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 23
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Check access before protected work, including on every app launch. Keep one stable client identity.
:::tabs
:::tab python Python
Request a fresh check (the SDK may use signed [offline grace](/docs/v1/start/glossary#offline-grace) after a network failure) with the same state directory:
```bash
.venv/bin/python license-check.py validate
```
For normal launch policy, use:
```bash
.venv/bin/python license-check.py check
```
The same file checks both validity and signature before reporting success.
:::
:::tab javascript JavaScript / Node.js
Request a fresh check (the SDK may use signed offline grace after a network failure) with the same state directory:
```bash
node license-check.mjs validate
```
For normal launch policy, use:
```bash
node license-check.mjs check
```
The same file checks both validity and signature before reporting success.
:::
:::tab php PHP
Request a fresh check (the SDK may use signed offline grace after a network failure) with the same state directory:
```bash
php license-check.php validate
```
For normal launch policy, use:
```bash
php license-check.php check
```
The same file checks both validity and signature before reporting success.
:::
:::tab csharp C# / .NET
Request a fresh check (the SDK may use signed offline grace after a network failure) with the same state directory:
```bash
dotnet run --project Guide.csproj -- validate
```
For normal launch policy, use:
```bash
dotnet run --project Guide.csproj -- check
```
The same file checks both validity and signature before reporting success.
:::
:::tab curl cURL / other language
Use your REST project's validation method with its stored [activation token](/docs/v1/start/glossary#activation-token). Verify the returned signature and bindings before granting access. Raw cURL only tests transport; it does not supply a safe allow/deny decision.
Use the selected language SDK or REST client from [SDKs and languages](/docs/v1/sdks/overview), its [signature verifier](/docs/v1/security/verify-ed25519), and the supported [webhook verifier](/docs/v1/guides/verify-webhook-signatures).
:::
:::
| Result | Your app does |
|---|---|
| Valid, signature verified, required [feature](/docs/v1/start/glossary#feature) enabled | Allow only that feature. |
| Expired, revoked, suspended or cancelled | Deny protected work; explain how to contact the publisher. |
| Invalid signature or inconsistent binding | Deny access; collect a safe [request ID](/docs/v1/start/glossary#request-id) for support. |
| Network failure | Follow the verified, unexpired cache policy in Stage 10; never invent permission. |
The sample prints an access decision. Replace that print with your protected action only after adding the feature check described on the SDK page. A valid [license](/docs/v1/start/glossary#license) does not imply every feature is included.
## Expected result
validate prints [Sandbox](/docs/v1/start/glossary#sandbox) license verified. while the license is valid. Deactivation is a release acknowledgement, never a grant.
## If it does not work
Reusing a different state directory makes a different installation. Check the chosen [product](/docs/v1/start/glossary#product), environment, clock and current license status before asking the [customer](/docs/v1/start/glossary#customer) to reinstall.
## Stage checklist
- Every protected entry point checks access.
- Required feature checks follow verified validity.
## Next steps
[Continue to Stage 9: Check the signed decision](/docs/v1/integration-guide/signatures)
# Webhook security
Canonical: https://licensing.kdpika.com/docs/v1/security/webhook-security
---
title: "Webhook security"
description: "Verify exact raw HMAC bytes, reject stale or duplicate events and coordinate endpoint-secret rotation safely."
slug: "/docs/v1/security/webhook-security"
section: "Security"
order: 23
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Treat a [webhook](/docs/v1/start/glossary#webhook) as a signed notification, not as the authority that decides whether a [license](/docs/v1/start/glossary#license) is valid.
## What you will learn
- Verify the sender's exact signature format.
- Apply receiver timestamp and deduplication rules.
- Rotate secrets without assuming an overlap.
## Prerequisites
- Read [authentication](/docs/v1/start/authentication).
- Use an HTTPS backend endpoint that can read the exact raw request body.
- Keep the endpoint secret on your server only.
## Exact sender contract
| Item | Actual value |
| --- | --- |
| Method | POST to your configured HTTPS endpoint |
| Content-Type | application/json |
| Event ID header | X-LMP-Event-ID |
| Event name header | X-LMP-Event |
| Signature header | X-LMP-Signature: t=UNIX_SECONDS,v1=LOWERCASE_HEX_HMAC |
| Signed message | decimal timestamp + a literal period + exact raw body bytes |
| Algorithm | HMAC-SHA256 with the endpoint's plaintext secret |
| Body | id, event, created_at, environment and data |
The X-LMP header names preserve existing compatibility identifiers. This signature is not Ed25519 and is not Stripe's signing format. Do not parse/reserialize the body before checking it.
## Receiver checks
1. Reject oversized bodies before buffering them.
2. Parse one supported timestamp/signature pair from the signature header.
3. Require a numeric Unix timestamp within your chosen tolerance.
4. Recompute HMAC-SHA256 over timestamp.period.raw_body with your endpoint secret.
5. Compare fixed-length signature bytes using a constant-time operation.
6. Parse the authenticated JSON and verify its event ID, environment and expected resource ownership.
7. Atomically record the unique event ID together with the intended local business change.
8. Return a 2xx response quickly after the action is durable.
Expected result: altered and stale messages fail; duplicate deliveries do not repeat the business effect.
A 300-second timestamp tolerance is receiver guidance, not a sender-enforced inbound gate. Keep your server clock synchronized and choose your own justified tolerance. Timestamp checks alone do not stop duplicates inside that window.
## Retries and ordering
The sender attempts at most 6 deliveries. Delays after attempts 1–5 are60, 300, 1800, 7200 and 43200 seconds, measured after failure handling. A 2xx response with no transport error means success. Lost acknowledgements can repeat [delivery](/docs/v1/start/glossary#delivery); ordering is not guaranteed.
The timestamp/signature is generated for each attempt, while the event ID and stored body remain stable. Deduplicate by authenticated event ID, not signature or delivery timestamp. Your unique-record/business-action transaction is the receiver's responsibility.
## Endpoint and secret protection
Use HTTPS with a valid certificate. The sender validates public DNS/IP targets, verifies TLS, does not follow redirects and applies5-second connect /12-second total timeouts. These sender controls do not replace authentication on your receiver.
There is no published fixed outbound IP range you can safely invent for an allowlist. Use HMAC verification as the primary sender-authentication control. If you add network filtering, obtain confirmed operational ranges and retain signature checks.
Endpoint secrets are hashed and encrypted for server delivery. Rotation replaces the active secret immediately; old queued events use the new current secret on their next attempt. No secret-version header or automatic overlap is implemented. Coordinate the receiver's secret update and securely remove retired values. Disabling an endpoint pauses queued attempts rather than erasing them.
## Reference boundary
This page is the complete security summary. The detailed event/delivery reference is assigned to P8 and remains unpublished until tested. Its prepared navigation target will activate when that phase publishes it; this page does not send readers to a broken or authenticated placeholder.
[Detailed webhook signature reference](/docs/v1/guides/verify-webhook-signatures)
## Verify your receiver
1. Send a signed synthetic test body from your own backend test harness.
2. Change one body byte and confirm refusal.
3. Use an old/future timestamp beyond the chosen tolerance and confirm refusal.
4. Send the same authenticated event ID twice and confirm one durable business effect.
5. Simulate an acknowledgement loss and confirm retries remain harmless.
6. Rotate only a test endpoint secret and confirm the intended coordinated update.
Expected result: receiver behavior is tested without third-party request catchers or production data. Complete receiver sample applications and event payload reference belong to P8, not an untested claim here.
## Expected result
A receiver validates authentic recent bytes and applies each authorized event once in its own durable transaction.
## Common mistakes
- Verifying a reserialized body.
- Trusting the event-name header without matching authenticated body data.
- Treating a license webhook or checkout return as the license-validity authority.
## Security notes
Do not include secrets in webhook URLs or public request-catcher tools. Notifications can be delayed; use the licensing API and verified state for access decisions.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| HMAC differs | Body bytes or secret changed | Verify the original raw body/current secret |
| Duplicate business action | No durable event-ID deduplication | Make the action and dedupe record atomic |
| Queued event fails after rotation | Sender uses the new secret immediately | Coordinate receiver configuration |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Developer data protection responsibilities
Canonical: https://licensing.kdpika.com/docs/v1/security/developer-data-protection
---
title: "Developer data protection responsibilities"
description: "Understand actual customer data, export and retention controls without mistaking pseudonymisation or account closure for complete erasure."
slug: "/docs/v1/security/developer-data-protection"
section: "Security"
order: 24
audience: "all"
level: "advanced"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"]
draft: false
---
Collect only [customer](/docs/v1/start/glossary#customer) information needed for licensing and document how your application and KDPika handle it.
## What you will learn
- Identify stored and signed customer/[device](/docs/v1/start/glossary#device) data.
- Use export and privacy-request workflows accurately.
- Separate confirmed controls from unresolved provider/legal facts.
## Prerequisites
- Read [the public privacy notice](/legal/privacy) and [DPA](/legal/dpa).
- Know what customer information your own store/application sends.
- Review your intended legal roles with qualified advice.
## Customer and device information
| Data | Current use / location | Your responsibility |
| --- | --- | --- |
| Customer email and optional name/company/country/external ID | Workspace customer records, [license](/docs/v1/start/glossary#license) associations and selected signed payload fields | Have an appropriate basis, disclose the purpose and minimize optional fields |
| License state, [product](/docs/v1/start/glossary#product)/plan and masked key | License management, signed snapshots and export | Limit access and avoid logging complete secrets |
| Device ID derivatives and optional labels/platform/version | Database HMAC binding, signed SHA-256 installation hash | Use a random [installation ID](/docs/v1/start/glossary#installation-id); avoid hardware fingerprints |
| API activity | [Request ID](/docs/v1/start/glossary#request-id), method, endpoint, status, duration and HMAC of remote IP | Protect your own correlated logs and avoid unnecessary personal data |
| Support/privacy requests | Workspace support and rights-request workflow | Do not submit unrelated sensitive data |
| Platform billing information | [Developer](/docs/v1/start/glossary#developer) [platform subscription](/docs/v1/start/glossary#platform-subscription)/invoices through Stripe or Mollie | Distinguish your own SaaS billing from payments by your software buyers |
The licensing API logger does not store request bodies or Authorization values. That is not a promise that no other application, hosting or provider log exists. Hashing an identifier is pseudonymisation in many contexts, not automatic anonymisation. See [CNIL's guidance on identifying personal data](https://www.cnil.fr/fr/node/698).
## Data roles and notices
Your store and application determine what buyer data you collect and why. Review whether you act as controller and when KDPika acts as processor for customer licensing data; the operator also has separate account/billing responsibilities. Do not copy a generic role label without checking your actual arrangement.
Document purpose, data categories, recipients, retention, rights handling, security and international transfers where applicable. Keep the public DPA/subprocessor/legal texts under review; their publication is not a compliance certification or legal approval.
## Export
1. Sign in to the intended workspace and open Settings.
2. Request the account/data export through the available export action.
3. Save it in restricted storage.
4. Confirm the [organization](/docs/v1/start/glossary#organization), members, subscription, products, customers, masked licenses, invoices, support tickets and privacy requests belong to that workspace.
5. Remove the local copy when your justified use ends.
Expected result: tenant-scoped export, excluding password hashes, API secrets and signing keys. It is not an export of every possible backup, log or third-party provider record.
## Rights requests and deletion
The workflow accepts access, rectification, erasure, restriction, portability and objection requests. Details are limited to5000 characters; the stored workflow target is30 days. That target does not prove every request is automatically fulfilled or replace legal deadline assessment.
Account closure soft-deletes the organization/user and anonymizes user display information. It does not comprehensively erase every related customer, license, financial row, backup or provider record. Do not promise immediate complete erasure after clicking close.
1. Identify the requester and the relevant workspace/customer proportionately.
2. Record the request through the supported privacy workflow.
3. Review the data and applicable retention/legal exceptions.
4. Coordinate required changes with support at hi@kdpika.com.
5. Document what was completed, retained with a reason, or requires provider action.
Expected result: a traceable human-reviewed response, not an invented automated deletion capability.
## Retention and providers
API/audit retention follows platform-plan configuration. Delivered mail/[webhook](/docs/v1/start/glossary#webhook) records have a30-day cleanup rule; other tokens and registration/idempotency records have their own expiry cleanup. Actual cleanup depends on scheduled jobs. A configured rule is not proof a production cron job ran.
The code supports Hostinger-compatible hosting, Stripe/Mollie platform billing and SMTP profiles for Hostinger/Titan, Brevo and custom SMTP. Source alone does not confirm the actual hosting contracting entity, data-centre region, support-access countries, selected production SMTP profile, signed DPAs or transfer safeguards. France is the operator's country, not proof all processing remains in France or the EU.
Confirm actual providers and contracts before promising EU-only processing. Review [subprocessor information](/legal/subprocessors) and ask support for unresolved operational facts. No SOC 2, ISO 27001, independent audit, response-time SLA or complete GDPR certification is claimed.
## Application checklist
1. Send only needed customer fields.
2. Avoid raw IDs/tokens in public logs and screenshots.
3. Keep local signed caches private because they contain customer data.
4. Explain installation binding and offline behavior in your own notice.
5. Test export and rights workflows with synthetic data.
6. Maintain your own store/application retention and breach-response process.
Expected result: customer data protection is part of your integration design, not delegated to a licensing signature.
## Expected result
You can explain which data is processed, what export/deletion actually does and which provider facts still require confirmation.
## Common mistakes
- Calling hashed device IDs anonymous.
- Treating account closure as comprehensive erasure.
- Claiming France-based operation guarantees EU-only data hosting.
## Security notes
Legal obligations depend on your processing and jurisdiction. This technical description is not legal advice. Never send private keys, payment-card details or unrelated sensitive records to support.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Export lacks raw secrets | Intentional exclusion | Retrieve/rotate credentials through supported secure workflows |
| Related rows remain after closure | Soft-deletion is not comprehensive erasure | Submit/review the applicable privacy request |
| Hosting region is unclear | Not established by source | Obtain confirmed provider/contract information |
## Next steps
Read [signed responses](/docs/v1/security/signed-responses) and [the threat model](/docs/v1/security/threat-model), then validate your own integration in [Sandbox](/docs/v1/start/glossary#sandbox).
## Related
- [Verification examples](/docs/v1/security/verify-ed25519)
- [Offline mode](/docs/v1/security/offline-mode)
- [Security and disclosure](/security)
# Check the signed decision
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/signatures
---
slug: "/docs/v1/integration-guide/signatures"
title: "Check the signed decision"
description: "Stage 9: check the signed decision with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 24
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Ensure a fake “licensed” response cannot unlock your app.
A signature is the server's proof that a decision has not been changed. The [public verification key](/docs/v1/start/glossary#public-verification-key) is not a [developer API key](/docs/v1/start/glossary#developer-api-key).
The four [official SDKs](/docs/v1/start/glossary#official-sdk) fetch the public key, verify Ed25519 and check the signed decision against the application, environment, installation and time. The Stage 7 sample also requires the SDK's signature-valid flag. Do not replace this with a success field from raw JSON.
:::tabs
:::tab python Python
Confirm result.valid and result.signature_valid before result.[feature](/docs/v1/start/glossary#feature)("pdf_export"). The sample already requires the first two flags.
:::
:::tab javascript JavaScript / Node.js
Confirm result.valid and result.signatureValid before result.feature('pdf_export'). The sample already requires the first two flags.
:::
:::tab php PHP
Confirm $result->valid and $result->signatureValid before $result->feature('pdf_export'). The sample already requires the first two flags.
:::
:::tab csharp C# / .NET
Confirm result.Valid and result.SignatureValid before result.Feature("pdf_export"). The sample already requires the first two flags.
:::
:::tab curl cURL / other language
REST transport does not supply a universal verified decision. Fetch the public key, canonicalize the original JSON correctly, verify the Ed25519 signature and all signed bindings, then inspect features. Use the real language verifier and negative test vectors, not a new handwritten crypto shortcut.
:::
:::
Use the selected language SDK or REST client from [SDKs and languages](/docs/v1/sdks/overview), its [signature verifier](/docs/v1/security/verify-ed25519), and the supported [webhook verifier](/docs/v1/guides/verify-webhook-signatures).
## Expected result
A modified payload/signature fails closed. The selected SDK/client's negative signature tests must pass for the payload shapes your [product](/docs/v1/start/glossary#product) uses.
## If it does not work
Known Python separator/exponent cases, JavaScript integral-float cases and C# Unicode cache cases are documented in [Current integration limitations](/docs/v1/integration-notes). A narrow successful example does not remove these defects. Do not ship affected cases or bypass verification; seek a compatible release/assessed verifier before [Live](/docs/v1/start/glossary#live).
## Stage checklist
- Tampering is denied.
- Public-key retrieval and rotation policy are understood.
- My production payloads pass the compatibility/test-vector gate.
## Next steps
[Continue to Stage 10: Handle errors and offline use](/docs/v1/integration-guide/real-world)
# API conventions
Canonical: https://licensing.kdpika.com/docs/v1/api/conventions
---
title: "API conventions"
description: "Use KDPika API v1 with the correct credentials, JSON envelopes, request IDs, environment boundaries and safe retry assumptions."
slug: "/docs/v1/api/conventions"
section: "API reference"
order: 25
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication","/docs/v1/security/signed-responses"]
draft: false
---
Use the documented licensing operations over HTTPS and distinguish transport success, authenticated [license](/docs/v1/start/glossary#license) state and management actions.
## What you will learn
- Choose the base URL, credential and environment for each operation.
- Interpret response envelopes and [request IDs](/docs/v1/start/glossary#request-id).
- Avoid assuming pagination, browser CORS or universal retry guarantees.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication).
- Keep [developer API keys](/docs/v1/start/glossary#developer-api-key) on trusted servers only.
## Base URL and version
The production API base is https://licensing.kdpika.com/api/v1. Endpoint paths shown as /licenses are relative to that base. SDK constructors use the site origin without /api/v1; do not append the version twice.
Use HTTPS with certificate and hostname verification. Do not disable TLS checks or silently follow a redirect to another origin carrying credentials. Loopback HTTP is used only by the isolated local test environment, not as a production integration setting.
The route prefix identifies API v1, independently of SDK package versions. Do not assume a requested /api/v2 path exists. Use the versioned contract and inspect documented compatibility limitations when updating a client.
## Headers and credential boundaries
| Header | Use | Notes |
| --- | --- | --- |
| Accept: application/json | Recommended on every API request | Responses use JSON; this is not content negotiation for HTML |
| Content-Type: application/json | Send for JSON bodies | Create reads decoded JSON directly; client actions read JSON when this media type is present |
| X-App-ID | Public [activation](/docs/v1/start/glossary#activation) | The public app_ identifier, not the internal [product](/docs/v1/start/glossary#product) ID or slug |
| X-Environment | Activation | SANDBOX by default; uppercase-normalized SANDBOX or [LIVE](/docs/v1/start/glossary#live) |
| Authorization: Bearer act_ credential | Validate/deactivate | Token determines installation/product/environment |
| X-Installation-ID | Validation | Transport-optional, but always send the same persisted 16–255 character ID; clients require its signed binding |
| Authorization: Bearer lmp_ credential | Management operations | Key fixes product, [organization](/docs/v1/start/glossary#organization), environment and [scopes](/docs/v1/start/glossary#scope) |
| Idempotency-Key | Required on create only | Trimmed 8–190 characters; not a universal mutation retry mechanism |
| User-Agent | Recommended | Identify your application/version without secrets or [customer](/docs/v1/start/glossary#customer) data |
X-Environment does not override a [developer](/docs/v1/start/glossary#developer) key's environment or an [activation token](/docs/v1/start/glossary#activation-token)'s binding. Public-key retrieval has no environment selector. The [public App ID](/docs/v1/start/glossary#public-app-id) can be shared across environments; customer/license/key/[webhook](/docs/v1/start/glossary#webhook) state is isolated according to the product contract.
Header names are case-insensitive. Bearer scheme matching is case-insensitive, but credential values must remain exact. Do not put credentials in query strings, URLs, documentation feedback or browser-delivered management code.
## JSON request behavior
Send objects using the exact documented field names and types. Optional unknown fields are ignored; that does not create a metadata, bulk or feature-management API. Request parsing is permissive in the current backend: malformed/scalar JSON becomes an empty array of inputs rather than a dedicated INVALID_JSON licensing error. Required-input validation then determines the actual error, or an operation with no consumed body fields may still succeed.
The string validator trims and casts PHP values rather than enforcing strict JSON types. Do not depend on that coercion. Invalid optional descriptive lengths may become empty/null rather than reject the whole request. Detailed endpoint constraints describe these differences; the OpenAPI recommended-input schema is not a claim that every malformed alternate representation is rejected identically.
Create always reads JSON. Other input-consuming client/status/renew actions select JSON when Content-Type contains application/json; otherwise they read merged query/form fields with POST taking precedence. Prefer JSON consistently. There is no documented bulk request body or batch operation.
## Response envelopes
| Response | Shape and interpretation |
| --- | --- |
| Successful key retrieval | success=true, key, request_id |
| Successful create | success=true, license with the new license_key, request_id; HTTP 201; a stored replay also returns this key |
| Successful activation | success=true, activation_token, signed license, request_id |
| Successful validation | success=true, valid=true, signed license, request_id |
| Successful deactivation | success=true, deactivated=true, request_id |
| Successful management read | success=true, unsigned license summary, request_id |
| Successful state/renew/reset action | success=true, documented result fields, request_id |
| Licensing error | success=false, error.code, error.message, request_id with the actual non-2xx HTTP status |
The success flag alone does not authorize a paid [feature](/docs/v1/start/glossary#feature). Verify the signed license and enforce product/environment/installation/status/time/entitlement checks. A management summary is not an Ed25519 envelope. Deactivation success confirms release; it is not an active license payload.
The server encodes UTF-8 JSON without escaped slashes, preserves integral floats and emits Unicode directly with the documented separator exceptions. Preserve numeric tokens for [signature verification](/docs/v1/security/signed-responses); a normal JavaScript number round trip can change the signed bytes.
## Request IDs and timestamps
Every routed licensing response has an X-Request-ID header. JSON success/error bodies carry request_id. Record the current header, HTTP status, method/path and UTC time for support; redact Authorization, [license keys](/docs/v1/start/glossary#license-key), activation tokens and customer payloads.
An idempotent create replay returns the original stored JSON body, including its original request_id, while the response header identifies the current request. Keep both values when reporting replay issues. A request ID is correlation metadata, not a credential or proof that a retry is safe.
Response builders emit ISO8601 UTC-labelled timestamps with an explicit offset. Nullable dates remain null, not empty strings. Signed server_time, next_validation_at and offline_grace_until have their own policy meaning. A known non-UTC database-session discrepancy can affect stored display timestamps such as created_at; do not treat that field as a freshness proof.
## Lists, query parameters and sorting
The currently registered API v1 has 11 operations. There is no public license-list, customer CRUD, product CRUD, plan/feature CRUD, webhook-management or device-list endpoint. Manage those resources in the supported workspace UI.
No v1 operation implements pagination, cursor, page, limit, filtering or sorting query parameters. Do not invent ?page=, ?status= or a list response schema. Path license identifiers are opaque resource IDs scoped by the developer key; a mismatched/unknown ID returns the documented not-found response.
## Browser CORS and server clients
The licensing API does not publish Access-Control-Allow-Origin or an OPTIONS preflight handler. Wrong methods/unknown /api/ routes return NOT_FOUND404, not a promised405. A cross-origin browser fetch with custom credentials/headers is therefore not a supported integration route. Use a trusted backend or the supported desktop/Node runtime; never solve CORS by shipping an lmp_ key to a browser.
The documentation's Sandbox console is same-origin and keeps credentials in transient memory. It does not add permissive CORS, proxy credentials or make the API a browser-safe management service.
## Retry and idempotency boundary
Create requires an organization-scoped [idempotency key](/docs/v1/start/glossary#idempotency-key) retained24hours. Identical decoded-body hashes replay the stored response; changed content or member order can cause IDEMPOTENCY_CONFLICT409. Use a stable business-event identity and preserve the same request representation.
Current creation idempotency has recorded expired-row/concurrency defects. An expired key before cleanup or simultaneous creates can produce duplicate committed work and a500. Do not describe it as an exactly-once fulfillment guarantee. Reconcile uncertain creation results with your durable fulfillment records and support rather than blindly issuing a new key.
Other operations ignore Idempotency-Key. Activation rotates a token, renewal extends time again, deactivation repeats401 and repeated status/reset operations can emit audit/events again. A timeout after a mutation is an uncertain result, not proof nothing happened.
Rate-limit errors use429. Licensing errors currently omit Retry-After and success limiter headers; do not assume headers from the billing UI apply here. Successful limited calls expose X-RateLimit-Remaining and X-RateLimit-Reset. Use bounded backoff with jitter and operation-specific reconciliation, not immediate loops.
## Expected result
Requests use the correct origin/version, credential and fields; failure handling preserves the actual HTTP/error/request-ID information without exposing secrets.
## Common mistakes
- Using an internal product ID in X-App-ID, or a public App ID where create expects an internal ID/slug.
- Appending /api/v1 to an SDK origin twice.
- Assuming every mutation has safe replay or every wrong method returns405.
- Expecting unsupported list/query/CORS behavior.
## Security notes
Send management credentials only from your backend. Do not authorize a license from a webhook or checkout-success URL. Preserve signed bytes and fail closed on verification problems. The console and examples do not change the actual server's authentication, plan gates or known defects.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| NOT_FOUND404 | Wrong path, method or version | Match the exact documented operation |
| INVALID_REQUEST422 | Missing/invalid consumed fields or creation policy rejection | Correct the request; do not retry unchanged input blindly |
| Browser preflight fails | No cross-origin CORS contract | Move the operation to your backend or supported client runtime |
| Replay body and header IDs differ | Stored response versus current request | Retain both IDs for correlation |
## Next steps
Review [signed responses](/docs/v1/security/signed-responses) and [the working quickstart](/docs/v1/start/quickstart) before sending real credentials.
## Related
- [Authentication](/docs/v1/start/authentication)
- [API key leak response](/docs/v1/security/api-key-leak-response)
- [Installation identity](/docs/v1/security/installation-id)
# Handle errors and offline use
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/real-world
---
slug: "/docs/v1/integration-guide/real-world"
title: "Handle errors and offline use"
description: "Stage 10: handle errors and offline use with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 25
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Handle failures without trapping the [customer](/docs/v1/start/glossary#customer) or weakening access checks.
| What you see | What to do |
|---|---|
| INVALID_LICENSE | Check the full key, [public App ID](/docs/v1/start/glossary#public-app-id), [Sandbox](/docs/v1/start/glossary#sandbox) selection and matching customer email. |
| MAX_DEVICES_REACHED | Deactivate an old installation or inspect [Devices](/docs/v1/start/glossary#device). Do not make random identities. |
| SIGNATURE_INVALID / cached signature error | Keep access locked. Check the runtime, clock and SDK compatibility notes; do not disable verification. |
| NOT_ACTIVATED | Run activate in the same private state directory first. |
| NETWORK_ERROR / no connection | Reconnect. A first [activation](/docs/v1/start/glossary#activation) needs internet; no cached response means no offline grant. |
| LICENSE_EXPIRED / LICENSE_REVOKED / LICENSE_SUSPENDED | Explain the state to the customer and provide support; never silently convert it to valid. |
For normal launches, check can reuse a verified cache before its signed next_validation_at. After a network failure, any offline decision must still verify its signature, bindings, clock and offline_grace_until. Grace is based on signed server time, not “another N days after failure”.
1. Keep first activation online.
2. Use the same sample's check command to exercise the SDK's real cache policy.
3. Treat missing, corrupted, stale or clock-rollback cache as a denial requiring reconnection.
4. To move computers, run deactivate on the old installation while online, then activate on the new one.
:::tabs
:::tab python Python
Release this installation:
```bash
.venv/bin/python license-check.py deactivate
```
Expected output: Device released. This action does not grant access. Use activate on the replacement installation.
:::
:::tab javascript JavaScript / Node.js
Release this installation:
```bash
node license-check.mjs deactivate
```
Expected output: Device released. This action does not grant access. Use activate on the replacement installation.
:::
:::tab php PHP
Release this installation:
```bash
php license-check.php deactivate
```
Expected output: Device released. This action does not grant access. Use activate on the replacement installation.
:::
:::tab csharp C# / .NET
Release this installation:
```bash
dotnet run --project Guide.csproj -- deactivate
```
Expected output: Device released. This action does not grant access. Use activate on the replacement installation.
:::
:::tab curl cURL / other language
Use your REST project's deactivation operation with the stored [activation token](/docs/v1/start/glossary#activation-token). Keep the installation identity stable and do not treat a release response as an access grant.
Use the selected language SDK or REST client from [SDKs and languages](/docs/v1/sdks/overview), its [signature verifier](/docs/v1/security/verify-ed25519), and the supported [webhook verifier](/docs/v1/guides/verify-webhook-signatures).
:::
:::
Do not erase local files as a substitute for server deactivation; that can abandon a device seat. Reset devices in the dashboard only after an authenticated support decision.
## Expected result
Customers get a clear message: “We could not check your [license](/docs/v1/start/glossary#license). Reconnect or contact support.” They never see raw keys, tokens or stack traces.
## If it does not work
An offline denial can be a correct security outcome. The current C# Unicode cache issue needs explicit testing; permission-protected files are not an OS vault.
## Stage checklist
- I tested a disconnected launch and an expired cache.
- I tested a full device limit and a successful release.
- I keep unknown or unverified states locked.
## Next steps
[Continue to Stage 11: Protect tokens and identity](/docs/v1/integration-guide/storage)
# Retrieve the public key
Canonical: https://licensing.kdpika.com/docs/v1/api/public-key
---
title: "Retrieve the public key"
description: "Retrieve the current public verification key."
slug: "/docs/v1/api/public-key"
section: "API reference"
order: 26
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Retrieve the current [public verification key](/docs/v1/start/glossary#public-verification-key).
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
GET /api/v1/public-key
### When to use it
Fetch the current Ed25519 verification key during a trusted setup or controlled key refresh. This endpoint is global, not specific to an [organization](/docs/v1/start/glossary#organization) or environment.
## Authentication and environment
No credential or environment header is required.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
## 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 request body is consumed. Send this GET request without a body.
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](/docs/v1/api/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.
```json
{
"success": true,
"key": {
"algorithm": "Ed25519",
"key_id": "ed25519_2b9bc7e214b1209f",
"public_key": "3VNX3t2NIvzg37o1w-bjxJdJVFtgbfpR-_Q80u4U5tQ",
"encoding": "base64url"
},
"request_id": "req_d9e278f6722a692a5dc3bbe8"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| key | object | yes | See description | Current global verification key, not a historical key ring. | Nested object/array shown in full JSON |
| key.algorithm | string | yes | const="Ed25519" | Fixed verification algorithm. | "Ed25519" |
| key.key_id | string | yes | pattern="^ed25519_[a-f0-9]{16}$" | ed25519_ plus first16 hex SHA256 characters of raw public key. | "ed25519_2b9bc7e214b1209f" |
| key.public_key | string | yes | pattern="^[A-Za-z0-9_-]{43}$" | Unpadded base64url of32 raw Ed25519 public-key bytes. | "3VNX3t2NIvzg37o1w-bjxJdJVFtgbfpR-_Q80u4U5tQ" |
| key.encoding | string | yes | const="base64url" | Raw-key encoding. | "base64url" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_d9e278f6722a692a5dc3bbe8" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only 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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
No [license](/docs/v1/start/glossary#license), [device](/docs/v1/start/glossary#device), token, customer, audit event, [webhook](/docs/v1/start/glossary#webhook) or email is created. The public-key IP limiter is consumed. The global API metadata log has no organization attribution.
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
GET is safe to retry with bounded backoff. Cache the successful key for the advertised3600 seconds, subject to your pinned-key refresh policy. A timeout never justifies disabling verification. The endpoint returns only the current key, not a historical key ring.
## Rate limits
60 requests per60 seconds, keyed by the caller IP.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh public-key.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py public-key.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs public-key.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php public-key.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- public-key.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
Run node compiled/request.js public-key.
### TypeScript
```typescript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: TypeScript7.0.2, @types/node22.19.7, Node18.20+ native fetch, trusted server/Electron main process.
// Run compiled request.js with public-key after strict TypeScript compilation. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations: Readonly> = Object.freeze({
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
});
function required(name: string): string {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2] ?? '';
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers: Record = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run java -cp .:gson.jar Request public-key.
### Java
```java
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Java17+, Gson2.14.0. Run: java -cp .:gson.jar Request public-key
// Windows classpaths use semicolons. Expected: JSON; exit0 success/1 API error/2 setup or transport.
// Credentials/body: KDP_* environment variables. Private terminal only; output may contain secrets.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import com.google.gson.*;
public final class Request {
static final String CONTRACT = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""";
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) throw new IllegalArgumentException("Missing configuration.");
return value;
}
public static void main(String[] args) {
try {
JsonObject operations = JsonParser.parseString(CONTRACT).getAsJsonObject();
if (args.length != 1 || !operations.has(args[0])) throw new IllegalArgumentException("Unknown operation.");
String name = args[0]; JsonObject op = operations.getAsJsonObject(name);
String base = System.getenv().getOrDefault("KDP_BASE_URL", "https://licensing.kdpika.com/api/v1").replaceAll("/+$", "");
URI uri = URI.create(base);
boolean local = "http".equals(uri.getScheme()) && ("localhost".equals(uri.getHost()) || "127.0.0.1".equals(uri.getHost())) && "1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if (uri.getHost() == null || (!"https".equals(uri.getScheme()) && !local) || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null || !"/api/v1".equals(uri.getPath())) throw new IllegalArgumentException("Invalid base URL.");
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(base + op.get("path").getAsString())).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Java");
String auth = op.get("auth").getAsString();
if (auth.equals("publicApp")) request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX");
if (auth.equals("activation")) request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"));
if (name.equals("validate")) request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"));
if (op.get("method").getAsString().equals("POST")) {
String body = System.getenv().getOrDefault("KDP_BODY_JSON", "{}");
if (!JsonParser.parseString(body).isJsonObject()) throw new IllegalArgumentException("JSON object required.");
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else request.GET();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
System.out.write(response.body().getBytes(StandardCharsets.UTF_8)); System.out.write(10);
if (response.statusCode() < 200 || response.statusCode() >= 300 || !result.has("success") || !result.get("success").equals(new JsonPrimitive(true))) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); System.exit(1);
}
} catch (Exception error) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); System.exit(2);
}
}
}
```
Run go run request.go public-key.
### Go
```go
// Purpose: inspect one client-callable Sandbox operation, not grant access.
// Requires Go1.20+ standard library. Run: go run request.go public-key
// Expected: JSON, exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables; output may contain secrets. Private terminal only.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type operation struct {
Method string `json:"method"`
Path string `json:"path"`
Auth string `json:"auth"`
}
const contract = `{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}`
func required(name string) string {
v := os.Getenv(name)
if v == "" || strings.ContainsAny(v, "\r\n") {
panic("configuration")
}
return v
}
func execute() int {
var operations map[string]operation
if json.Unmarshal([]byte(contract), &operations) != nil || len(os.Args) != 2 {
panic("operation")
}
name := os.Args[1]
op, ok := operations[name]
if !ok {
panic("operation")
}
base := os.Getenv("KDP_BASE_URL")
if base == "" {
base = "https://licensing.kdpika.com/api/v1"
}
base = strings.TrimRight(base, "/")
u, err := url.Parse(base)
if err != nil {
panic("URL")
}
local := u.Scheme == "http" && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") && os.Getenv("KDP_ALLOW_LOOPBACK") == "1"
if u.Hostname() == "" || (u.Scheme != "https" && !local) || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/api/v1" {
panic("URL")
}
body := ""
if op.Method == "POST" {
body = os.Getenv("KDP_BODY_JSON")
if body == "" {
body = "{}"
}
var object map[string]json.RawMessage
if json.Unmarshal([]byte(body), &object) != nil || object == nil {
panic("body")
}
}
request, err := http.NewRequest(op.Method, base+op.Path, strings.NewReader(body))
if err != nil {
panic("request")
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "KDPikaDocsExample/1.0 Go")
if op.Method == "POST" {
request.Header.Set("Content-Type", "application/json")
}
if op.Auth == "publicApp" {
request.Header.Set("X-App-ID", required("KDP_APP_ID"))
request.Header.Set("X-Environment", "SANDBOX")
}
if op.Auth == "activation" {
request.Header.Set("Authorization", "Bearer "+required("KDP_ACTIVATION_TOKEN"))
}
if name == "validate" {
request.Header.Set("X-Installation-ID", required("KDP_INSTALLATION_ID"))
}
client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
response, err := client.Do(request)
if err != nil {
panic("transport")
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 1048577))
if err != nil || len(raw) > 1048576 {
panic("response")
}
var result struct {
Success bool `json:"success"`
}
if json.Unmarshal(raw, &result) != nil {
panic("JSON")
}
fmt.Println(string(raw))
if response.StatusCode < 200 || response.StatusCode >= 300 || !result.Success {
fmt.Fprintln(os.Stderr, "API rejection. Inspect error.code/request_id; reconcile mutations before retrying.")
return 1
}
return 0
}
func main() {
code := 2
func() {
defer func() {
if recover() != nil {
fmt.Fprintln(os.Stderr, "Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.")
}
}()
code = execute()
}()
os.Exit(code)
}
```
Run cargo run --locked --manifest-path rust/Cargo.toml -- public-key.
### Rust
```rust
// Purpose: inspect one client-callable Sandbox operation; not entitlement verification.
// Requires Rust1.82+, pinned Cargo.toml/Cargo.lock. Run: cargo run -- public-key
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, output may contain secrets.
use serde_json::Value;
use std::{env, process, time::Duration};
const CONTRACT: &str = r#"{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}"#;
fn required(name: &str) -> Result> {
let value = env::var(name)?;
if value.is_empty() || value.contains('\r') || value.contains('\n') {
return Err("configuration".into());
};
Ok(value)
}
fn execute() -> Result> {
let operations: Value = serde_json::from_str(CONTRACT)?;
let args: Vec = env::args().collect();
if args.len() != 2 {
return Err("operation".into());
}
let name = &args[1];
let op = operations.get(name).ok_or("operation")?;
let base = env::var("KDP_BASE_URL")
.unwrap_or_else(|_| "https://licensing.kdpika.com/api/v1".into())
.trim_end_matches('/')
.to_string();
let uri = url::Url::parse(&base)?;
let local = uri.scheme() == "http"
&& matches!(uri.host_str(), Some("127.0.0.1" | "localhost"))
&& env::var("KDP_ALLOW_LOOPBACK").unwrap_or_default() == "1";
if uri.host_str().is_none()
|| (uri.scheme() != "https" && !local)
|| !uri.username().is_empty()
|| uri.password().is_some()
|| uri.query().is_some()
|| uri.fragment().is_some()
|| uri.path() != "/api/v1"
{
return Err("URL".into());
}
let method = op["method"].as_str().ok_or("method")?;
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.redirects(0)
.build();
let target = base + op["path"].as_str().ok_or("path")?;
let mut request = agent
.request(method, &target)
.set("Accept", "application/json")
.set("User-Agent", "KDPikaDocsExample/1.0 Rust");
if op["auth"] == "publicApp" {
request = request
.set("X-App-ID", &required("KDP_APP_ID")?)
.set("X-Environment", "SANDBOX")
}
if op["auth"] == "activation" {
request = request.set(
"Authorization",
&("Bearer ".to_string() + &required("KDP_ACTIVATION_TOKEN")?),
)
}
if name == "validate" {
request = request.set("X-Installation-ID", &required("KDP_INSTALLATION_ID")?)
}
let answer = if method == "POST" {
let body = env::var("KDP_BODY_JSON").unwrap_or_else(|_| "{}".into());
if !serde_json::from_str::(&body)?.is_object() {
return Err("body".into());
}
request
.set("Content-Type", "application/json")
.send_string(&body)
} else {
request.call()
};
let response = match answer {
Ok(response) => response,
Err(ureq::Error::Status(_, response)) => response,
Err(error) => return Err(Box::new(error)),
};
let status = response.status();
let raw = response.into_string()?;
let result: Value = serde_json::from_str(&raw)?;
println!("{}", raw);
if !(200..300).contains(&status) || result["success"] != true {
eprintln!(
"API rejection. Inspect error.code/request_id; reconcile mutations before retrying."
);
return Ok(1);
}
Ok(0)
}
fn main() {
process::exit(match execute() {
Ok(code) => code,
Err(_) => {
eprintln!("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.");
2
}
})
}
```
```rust
[package]
name = "kdpika-api-example"
version = "1.0.0"
edition = "2021"
rust-version = "1.82"
[dependencies]
ureq = { version = "=2.12.1", default-features = false, features = ["tls"] }
url = "=2.5.2"
serde_json = "=1.0.145"
# Keep the tested Rust1.82 toolchain compatible with this pinned lock.
zeroize = "=1.8.1"
```
Run ruby request.rb public-key.
### Ruby
```ruby
# Purpose: inspect one client-callable Sandbox operation, not grant feature access.
# Requires Ruby2.6+ standard library. Run: ruby request.rb public-key
# Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
# Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
STDOUT.set_encoding(Encoding::UTF_8)
OPERATIONS = JSON.parse(<<~'JSON')
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
JSON
def required(name)
value = ENV.fetch(name, '')
raise 'configuration' if value.empty? || value.match?(/[\r\n]/)
value
end
begin
raise 'operation' unless ARGV.length == 1 && OPERATIONS.key?(ARGV[0])
name = ARGV[0]; op = OPERATIONS.fetch(name)
base = ENV.fetch('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').sub(%r{/+$}, '')
uri = URI.parse(base)
local = uri.scheme == 'http' && ['127.0.0.1','localhost'].include?(uri.host) && ENV['KDP_ALLOW_LOOPBACK'] == '1'
raise 'URL' unless uri.host && (uri.scheme == 'https' || local) && !uri.userinfo && !uri.query && !uri.fragment && uri.path == '/api/v1'
target = URI.parse(base + op.fetch('path'))
request = op.fetch('method') == 'GET' ? Net::HTTP::Get.new(target) : Net::HTTP::Post.new(target)
request['Accept'] = 'application/json'; request['User-Agent'] = 'KDPikaDocsExample/1.0 Ruby'
if op['auth'] == 'publicApp'
request['X-App-ID'] = required('KDP_APP_ID'); request['X-Environment'] = 'SANDBOX'
elsif op['auth'] == 'activation'
request['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
end
request['X-Installation-ID'] = required('KDP_INSTALLATION_ID') if name == 'validate'
if op['method'] == 'POST'
body = ENV.fetch('KDP_BODY_JSON', '{}'); raise 'JSON object' unless JSON.parse(body).is_a?(Hash)
request['Content-Type'] = 'application/json'; request.body = body
end
http = Net::HTTP.new(target.host, target.port, nil)
http.use_ssl = target.scheme == 'https'; http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 5; http.read_timeout = 15
response = http.request(request) # Net::HTTP does not follow redirects.
result = JSON.parse(response.body)
puts response.body
unless response.code.to_i.between?(200,299) && result['success'] == true
warn 'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.'; exit 1
end
rescue StandardError
warn 'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.'; exit 2
end
```
Run java -cp request.jar:gson.jar RequestKt public-key.
### Kotlin
```kotlin
// Purpose: inspect one client-callable Sandbox operation; not an entitlement verifier.
// Requires Kotlin2.4.21/JVM17+, Gson2.14.0. Run compiled RequestKt with public-key.
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, never application/CI logs.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import kotlin.system.exitProcess
val operationsJson = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""".trimIndent()
fun required(name: String): String = (System.getenv(name) ?: "").also {
require(it.isNotEmpty() && !it.contains('\r') && !it.contains('\n')) { "Missing configuration." }
}
fun main(args: Array) {
try {
val operations = JsonParser.parseString(operationsJson).asJsonObject
require(args.size == 1 && operations.has(args[0]))
val name = args[0]; val op = operations.getAsJsonObject(name)
val base = (System.getenv("KDP_BASE_URL") ?: "https://licensing.kdpika.com/api/v1").trimEnd('/')
val uri = URI.create(base)
val local = uri.scheme == "http" && uri.host in listOf("localhost", "127.0.0.1") && System.getenv("KDP_ALLOW_LOOPBACK") == "1"
require(uri.host != null && (uri.scheme == "https" || local) && uri.userInfo == null && uri.query == null && uri.fragment == null && uri.path == "/api/v1")
val request = HttpRequest.newBuilder(URI.create(base + op["path"].asString)).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Kotlin")
when (op["auth"].asString) {
"publicApp" -> request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX")
"activation" -> request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"))
}
if (name == "validate") request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"))
if (op["method"].asString == "POST") {
val body = System.getenv("KDP_BODY_JSON") ?: "{}"
require(JsonParser.parseString(body).isJsonObject)
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body))
} else request.GET()
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build()
val response = client.send(request.build(), HttpResponse.BodyHandlers.ofString())
val result = JsonParser.parseString(response.body()).asJsonObject
System.out.write(response.body().toByteArray(Charsets.UTF_8)); System.out.write(10)
if (response.statusCode() !in 200..299 || result["success"] != JsonPrimitive(true)) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); exitProcess(1)
}
} catch (error: Exception) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); exitProcess(2)
}
}
```
Run ./request public-key.
### Swift
```swift
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Swift5.7+, Foundation; FoundationNetworking on Linux.
// Build: swiftc -parse-as-library request.swift -o request; run: ./request public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let contract = #"""
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
"""#
enum SetupError: Error {case invalid}
func required(_ name:String) throws -> String {
let value=ProcessInfo.processInfo.environment[name] ?? ""
guard !value.isEmpty && !value.contains("\r") && !value.contains("\n") else {throw SetupError.invalid};return value
}
final class NoRedirect: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(_ session:URLSession,task:URLSessionTask,willPerformHTTPRedirection response:HTTPURLResponse,newRequest request:URLRequest,completionHandler:@escaping(URLRequest?)->Void){completionHandler(nil)}
}
@main struct Example {
static func main() async {
do {
let env=ProcessInfo.processInfo.environment
let operations=try JSONSerialization.jsonObject(with:Data(contract.utf8)) as! [String:[String:String]]
guard CommandLine.arguments.count==2,let op=operations[CommandLine.arguments[1]] else {throw SetupError.invalid}
let name=CommandLine.arguments[1]
let base=(env["KDP_BASE_URL"] ?? "https://licensing.kdpika.com/api/v1").replacingOccurrences(of:"/+$",with:"",options:.regularExpression)
guard let parts=URLComponents(string:base),let host=parts.host else {throw SetupError.invalid}
let local=parts.scheme=="http" && ["localhost","127.0.0.1"].contains(host) && env["KDP_ALLOW_LOOPBACK"]=="1"
guard (parts.scheme=="https" || local) && parts.user==nil && parts.password==nil && parts.query==nil && parts.fragment==nil && parts.path=="/api/v1",let url=URL(string:base+op["path"]!) else {throw SetupError.invalid}
var request=URLRequest(url:url);request.httpMethod=op["method"];request.timeoutInterval=15
request.setValue("application/json",forHTTPHeaderField:"Accept");request.setValue("KDPikaDocsExample/1.0 Swift",forHTTPHeaderField:"User-Agent")
if op["auth"]=="publicApp" {request.setValue(try required("KDP_APP_ID"),forHTTPHeaderField:"X-App-ID");request.setValue("SANDBOX",forHTTPHeaderField:"X-Environment")}
if op["auth"]=="activation" {request.setValue("Bearer "+(try required("KDP_ACTIVATION_TOKEN")),forHTTPHeaderField:"Authorization")}
if name=="validate" {request.setValue(try required("KDP_INSTALLATION_ID"),forHTTPHeaderField:"X-Installation-ID")}
if op["method"]=="POST" {
let body=Data((env["KDP_BODY_JSON"] ?? "{}").utf8)
guard try JSONSerialization.jsonObject(with:body) is [String:Any] else {throw SetupError.invalid}
request.httpBody=body;request.setValue("application/json",forHTTPHeaderField:"Content-Type")
}
let session=URLSession(configuration:.ephemeral,delegate:NoRedirect(),delegateQueue:nil)
defer {session.invalidateAndCancel()}
let (raw,response)=try await session.data(for:request)
guard let http=response as? HTTPURLResponse,let result=try JSONSerialization.jsonObject(with:raw) as? [String:Any],let text=String(data:raw,encoding:.utf8) else {throw SetupError.invalid}
_ = text // Decoding above checks UTF-8 without reserializing JSON.
FileHandle.standardOutput.write(raw);FileHandle.standardOutput.write(Data([10]))
guard (200..<300).contains(http.statusCode),result["success"] as? Bool == true else {
FileHandle.standardError.write(Data("API rejection. Inspect error.code/request_id; reconcile mutations before retrying.\n".utf8));exit(1)
}
} catch {
FileHandle.standardError.write(Data("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n".utf8));exit(2)
}
}
}
```
Run dart run request.dart public-key.
### Dart
```dart
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Dart3.7+ standard library. Run: dart run request.dart public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body. Private terminal only; output can contain secrets.
import 'dart:convert';
import 'dart:io';
const contract = r'''{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}''';
String required(String name) {
final value = Platform.environment[name] ?? '';
if (value.isEmpty || value.contains('\r') || value.contains('\n'))
throw StateError('configuration');
return value;
}
Future main(List args) async {
stdout.encoding = utf8;
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
try {
final operations = jsonDecode(contract) as Map;
if (args.length != 1 || !operations.containsKey(args[0]))
throw StateError('operation');
final name = args[0];
final op = operations[name] as Map;
final base =
(Platform.environment['KDP_BASE_URL'] ??
'https://licensing.kdpika.com/api/v1')
.replaceFirst(RegExp(r'/+$'), '');
final uri = Uri.parse(base);
final local =
uri.scheme == 'http' &&
['127.0.0.1', 'localhost'].contains(uri.host) &&
Platform.environment['KDP_ALLOW_LOOPBACK'] == '1';
if (uri.host.isEmpty ||
(uri.scheme != 'https' && !local) ||
uri.userInfo.isNotEmpty ||
uri.hasQuery ||
uri.hasFragment ||
uri.path != '/api/v1')
throw StateError('URL');
final request = await client
.openUrl(
op['method'] as String,
Uri.parse(base + (op['path'] as String)),
)
.timeout(const Duration(seconds: 15));
request.followRedirects = false;
request.headers.set('Accept', 'application/json');
request.headers.set('User-Agent', 'KDPikaDocsExample/1.0 Dart');
if (op['auth'] == 'publicApp') {
request.headers.set('X-App-ID', required('KDP_APP_ID'));
request.headers.set('X-Environment', 'SANDBOX');
}
if (op['auth'] == 'activation')
request.headers.set(
'Authorization',
'Bearer ${required('KDP_ACTIVATION_TOKEN')}',
);
if (name == 'validate')
request.headers.set('X-Installation-ID', required('KDP_INSTALLATION_ID'));
if (op['method'] == 'POST') {
final body = Platform.environment['KDP_BODY_JSON'] ?? '{}';
if (jsonDecode(body) is! Map) throw StateError('JSON object');
request.headers.set('Content-Type', 'application/json');
request.add(utf8.encode(body));
}
final response = await request.close().timeout(const Duration(seconds: 15));
final raw = await utf8.decoder
.bind(response)
.join()
.timeout(const Duration(seconds: 15));
final result = jsonDecode(raw) as Map;
stdout.writeln(raw);
if (response.statusCode < 200 ||
response.statusCode >= 300 ||
result['success'] != true) {
stderr.writeln(
'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.',
);
exitCode = 1;
}
} catch (error) {
stderr.writeln(
'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.',
);
exitCode = 2;
} finally {
client.close(force: true);
}
}
```
Run ./request-c public-key.
### C
```c
/* C11 entry point. Build with client_http.c include, yyjson0.13.0 and libcurl7.76+.
* Run request public-key; expected JSON, exit0 success/1 rejection/2 unavailable. */
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```c
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
Run ./request-cpp public-key.
### C++
```cpp
// C++17 entry point using the complete adjacent native transport.
// Requires yyjson0.13.0 and libcurl7.76+. Run request public-key; JSON/exit0,1,2.
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```cpp
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), internal [product](/docs/v1/start/glossary#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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Protect tokens and identity
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/storage
---
slug: "/docs/v1/integration-guide/storage"
title: "Protect tokens and identity"
description: "Stage 11: protect tokens and identity with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 26
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Keep one stable installation identity and protect its token/cache.
The example intentionally uses a private state directory. It demonstrates the SDK's file behavior, not complete production vault integration.
| Platform | Simplest correct starting point |
|---|---|
| Windows | A per-user LocalAppData directory with user-only ACLs; assess DPAPI or Credential Manager through an appropriate adapter. |
| macOS | A user-owned application-support directory; assess Keychain for the private token. |
| Linux | A user-owned application-data directory with restrictive permissions; assess Secret Service availability. |
| Shared backend | A private path outside the web root, isolated per application tenant/installation. |
Do not invent an adapter constructor option. The Python SDK can use its existing keyring support; the other current SDKs' file implementations require assessment before claiming a vault. No vault is installed by the sample.
1. Preserve the SDK-created installation identity through restarts.
2. Keep App ID/environment/state namespaces separate.
3. Never sync [activation tokens](/docs/v1/start/glossary#activation-token) through a public repository or distribute your activated test directory.
4. Serialize identity, [activation](/docs/v1/start/glossary#activation) and deactivation writes for the same installation.
Use the selected language SDK or REST client from [SDKs and languages](/docs/v1/sdks/overview), its [signature verifier](/docs/v1/security/verify-ed25519), and the supported [webhook verifier](/docs/v1/guides/verify-webhook-signatures).
## Expected result
A restarted app validates the same installation without consuming another seat. Another OS user cannot read its private files.
## If it does not work
Deleting identity state is not a [license](/docs/v1/start/glossary#license) reset. A customer-controlled desktop cannot be made tamper-proof; enforce valuable online operations on your authenticated backend too.
## Stage checklist
- I checked file permissions on every supported OS.
- My app does not ship a private activation token or [developer API key](/docs/v1/start/glossary#developer-api-key).
- Unsupported storage/runtime assumptions are resolved before [Live](/docs/v1/start/glossary#live).
## Next steps
[Continue to Stage 12: Deliver licenses to customers](/docs/v1/integration-guide/deliver)
# Create a license
Canonical: https://licensing.kdpika.com/docs/v1/api/create-license
---
title: "Create a license"
description: "Issue a license after your backend verifies the buyer purchase."
slug: "/docs/v1/api/create-license"
section: "API reference"
order: 27
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Issue a [license](/docs/v1/start/glossary#license) after your backend verifies the buyer purchase.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses
### When to use it
Issue a buyer license from your trusted backend after your own store verifies the purchase. KDPika does not collect payment from your software buyers or verify their store receipt on this endpoint.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
| Idempotency-Key | yes | Stable organization-scoped business-event key,8–190 trimmed characters; retention24hours, known expiry/concurrency limits. | YOUR_STABLE_BUSINESS_EVENT_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
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| product_id | string | yes | minLength=2; maxLength=190 | Internal ID or slug of the developer key product; not [public App ID](/docs/v1/start/glossary#public-app-id). | "YOUR_INTERNAL_PRODUCT_ID" |
| plan_id | string | yes | minLength=2; maxLength=140 | Active product-plan ID or slug. | "YOUR_PRODUCT_PLAN_ID" |
| customer | object | yes | See description | Customer fields; country/metadata are not consumed. | Nested object/array shown in full JSON |
| customer.email | string | yes | maxLength=190; format="email" | Normalized trimmed lowercase email; required. | "customer@example.invalid" |
| customer.name | string | no | maxLength=160 | Optional trimmed name; invalid length becomes empty. | "Example customer" |
| customer.company | string | no | maxLength=160 | Optional trimmed company; invalid length becomes empty. | "Example company" |
| customer.external_id | string | no | maxLength=190 | Optional external customer ID; invalid length becomes empty. | "YOUR_CUSTOMER_REFERENCE" |
### Full request JSON
```json
{
"product_id": "YOUR_INTERNAL_PRODUCT_ID",
"plan_id": "YOUR_PRODUCT_PLAN_ID",
"customer": {
"email": "customer@example.invalid",
"name": "Example customer",
"company": "Example company",
"external_id": "YOUR_CUSTOMER_REFERENCE"
}
}
```
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](/docs/v1/api/conventions) describes malformed JSON and alternate parsing.
## Success response
HTTP 201. The following is a real isolated Sandbox capture; secret values are replaced with explicit placeholders.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"license_key": "YOUR_NEW_LICENSE_KEY",
"expires_at": "2026-11-16T10:39:35+00:00",
"environment": "sandbox"
},
"request_id": "req_1c0f804fd7c455f25fa1f791"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | Newly issued license credential. | Nested object/array shown in full JSON |
| license.id | string | yes | See description | Created license identifier. | "lic_578581bd80c2a672da30fc29" |
| license.license_key | string | yes | See description | Complete newly generated secret; save securely. Returned on stored create replay too. | "YOUR_NEW_LICENSE_KEY" |
| license.expires_at | string or null | yes | format="date-time" | Calculated expiry. | "2026-11-16T10:39:35+00:00" |
| license.environment | string | yes | enum=["sandbox", "[live](/docs/v1/start/glossary#live)"] | Developer-key environment. | "sandbox" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_1c0f804fd7c455f25fa1f791" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | No blind retry; apply the correction |
| PLAN_NOT_FOUND | 404 | Create product/plan does not resolve within active API-key product | Correct scoped IDs | No blind retry; apply the correction |
| IDEMPOTENCY_CONFLICT | 409 | Organization key reused with different decoded-body hash (member order matters) | Use same body; new key only for new operation | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Creates one ACTIVE license and, if needed, one customer scoped by organization/environment/email. An existing customer's descriptive fields are not updated by this call. Duration/trial policy comes from the [product plan](/docs/v1/start/glossary#product-plan). Creates audit license.created and queues [webhook](/docs/v1/start/glossary#webhook) license.created for matching configured endpoints; no email is sent directly. A stored replay does not issue another license or repeat those creation events. Active-license capacity is checked before creation. The new plaintext key is included in the retained idempotency response for 24 hours; do not call this no-plaintext storage.
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
An 8–190 character Idempotency-Key is mandatory and scoped to the organization, not a single product. The retention window is24 hours. The hash covers PHP JSON encoding of the decoded body; changed member order can conflict. A stored replay returns 201, the original body/request_id/key and Idempotent-Replay:true, with a new current X-Request-ID header. Known expiry/concurrency defects can commit duplicate licenses before 500. Serialize fulfillment by business event on your backend, preserve your durable mapping and reconcile uncertain outcomes instead of blindly choosing a new key. This is not an exactly-once guarantee.
## Rate limits
Developer pre-authentication:240 requests per60 seconds per IP. After successful authentication/scope checks:60 per60 seconds per developer-key license-create bucket.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh create-license.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py create-license.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs create-license.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php create-license.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- create-license.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| INVALID_REQUEST (HTTP 422) | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry |
| PLAN_NOT_FOUND (HTTP 404) | Create product/plan does not resolve within active API-key product | Correct scoped IDs |
| IDEMPOTENCY_CONFLICT (HTTP 409) | Organization key reused with different decoded-body hash (member order matters) | Use same body; new key only for new operation |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Deliver licenses to customers
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/deliver
---
slug: "/docs/v1/integration-guide/deliver"
title: "Deliver licenses to customers"
description: "Stage 12: deliver licenses to customers with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 27
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Choose one [delivery](/docs/v1/start/glossary#delivery) route. Sending a key and protecting the software are separate tasks.
### No-code manual route
1. Open [Sandbox](/docs/v1/start/glossary#sandbox) **[Licenses](/docs/v1/start/glossary#license) → New license**.
2. Choose the [customer](/docs/v1/start/glossary#customer)'s [product](/docs/v1/start/glossary#product) and plan, enter their email and create the key.
3. Deliver it through your private customer channel. The full key is shown once.
4. Inspect [Devices](/docs/v1/start/glossary#device) and the license status for support, never request a password.
### WooCommerce
1. Install the official connector and connect your workspace.
2. Use **Import store products → Complete setup & enable sync** to prepare App IDs, plans and mappings together. Existing IDs are reused.
3. Review the shared device/access defaults once. Official subscription metadata comes from WooCommerce; inferred name-based periods need review.
4. Test a confirmed store order and verify the key in the order and My Account.
5. Keep Test/[Live](/docs/v1/start/glossary#live) payment evidence, quotas and product permissions enforced.
The native connector is different from the advanced standalone store-service reference.
### Another store or backend
Choose the matching [sales-platform guide](/docs/v1/sell). A [webhook](/docs/v1/start/glossary#webhook) is a server notification, not payment proof by itself.
1. Verify the provider notification's signature using original raw bytes.
2. Retrieve/confirm the current purchase with the provider and match product/customer/amount/state.
3. Issue or update through a scoped management API key on your trusted backend.
4. Save a durable purchase-to-license mapping and deduplicate retries.
5. Handle cancellations, paid-through access, refunds and disputes without blind reissuance.
No-code automation still needs a trusted verification/fulfillment step; do not paste a [developer](/docs/v1/start/glossary#developer) key into public browser code.
## Expected result
One eligible purchase produces the intended license, and repeated notifications do not duplicate it.
## If it does not work
Customer software license sales are separate from Stripe/Mollie billing for your KDPika [platform subscription](/docs/v1/start/glossary#platform-subscription). Delayed notifications and an unconfirmed payment must not unlock customer access.
## Stage checklist
- I chose one delivery route.
- Confirmed purchase and license ownership match.
- I tested duplicate delivery, cancellation and refund handling.
## Next steps
[Continue to Stage 13: Test the complete lifecycle](/docs/v1/integration-guide/test)
# Activate an installation
Canonical: https://licensing.kdpika.com/docs/v1/api/activate
---
title: "Activate an installation"
description: "Bind a customer license to one persisted installation."
slug: "/docs/v1/api/activate"
section: "API reference"
order: 28
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Bind a [customer](/docs/v1/start/glossary#customer) [license](/docs/v1/start/glossary#license) to one persisted installation.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep customer credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses/activate
### When to use it
Bind an eligible customer license to the persisted random [installation ID](/docs/v1/start/glossary#installation-id) of a desktop app, CLI, plugin host or trusted service. Include email if your chosen application flow requires a matching email; this API also supports key-only [activation](/docs/v1/start/glossary#activation).
## Authentication and environment
Use the public X-App-ID and a customer [license key](/docs/v1/start/glossary#license-key). X-Environment defaults to SANDBOX; the license/customer lookup is environment-scoped.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| X-App-ID | yes | [Public App ID](/docs/v1/start/glossary#public-app-id),8–80 trimmed characters. | YOUR_PUBLIC_APP_ID |
| X-Environment | no | Activation environment; backend normalizes case. Does not override token/developer-key bindings. | SANDBOX |
## 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
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| license_key | string | yes | minLength=12; maxLength=100 | Customer license key; case-insensitive hashed lookup. | "YOUR_CUSTOMER_LICENSE_KEY" |
| device_id | string | yes | minLength=16; maxLength=255 | Persisted random installation ID. | "YOUR_PERSISTED_INSTALLATION_ID" |
| email | string | no | maxLength=190 | Optional; nonempty normalized email must match the license customer. | "customer@example.invalid" |
| device_name | string | no | maxLength=160 | Optional descriptive label; invalid length becomes empty. | "Example workstation" |
| platform | string | no | maxLength=50 | Optional description, no OS enum. | "macOS" |
| app_version | string | no | maxLength=30 | Optional version; empty bypasses version-range check. | "1.0.0" |
### Full request JSON
```json
{
"license_key": "YOUR_CUSTOMER_LICENSE_KEY",
"device_id": "YOUR_PERSISTED_INSTALLATION_ID",
"email": "customer@example.invalid",
"device_name": "Example workstation",
"platform": "macOS",
"app_version": "1.0.0"
}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"activation_token": "YOUR_ACTIVATION_TOKEN",
"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_11a6f2cf9cfffdaf92e1a6ce"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| activation_token | string | yes | See description | act_ plus43 base64url characters; installation secret, do not log. | "YOUR_ACTIVATION_TOKEN" |
| license | object | yes | See description | Only payload is signed. This is PHP-specific canonical JSON, not RFC8785. | Nested object/array shown in full JSON |
| license.payload | object | yes | See description | Exact signed snapshot. Signature authenticity alone does not grant access. | Nested object/array shown in full JSON |
| license.payload.license_id | string | yes | See description | License public identifier. | "lic_578581bd80c2a672da30fc29" |
| license.payload.product_id | string | yes | See description | Public App ID; compare to configured [product](/docs/v1/start/glossary#product). | "app_a642cd24641ec976626b" |
| license.payload.product | string | yes | See description | Product display name. | "Docs fixture product" |
| license.payload.plan | string | yes | See description | Product-plan slug. | "fixture-plan" |
| license.payload.status | string | yes | enum=["active", "suspended", "revoked", "expired", "pending", "cancelled"] | Stored license status in lowercase. | "active" |
| license.payload.environment | string | yes | enum=["sandbox", "[live](/docs/v1/start/glossary#live)"] | Bound license environment. | "sandbox" |
| license.payload.expires_at | string or null | yes | format="date-time" | Independent license expiry; null has no configured expiry. | "2026-11-16T10:39:35+00:00" |
| license.payload.license_key_mask | string | yes | See description | Display-only masked key. | "TESTDOCS-••••-••••-HAHH" |
| license.payload.starts_at | string or null | yes | format="date-time" | Configured license start, currently not enforced by server eligibility. | "2026-10-17T10:39:35+00:00" |
| license.payload.created_at | string or null | yes | format="date-time" | Creation timestamp; database-timezone discrepancy is disclosed. | "2026-10-17T12:39:35+00:00" |
| license.payload.max_devices | integer | yes | See description | Concurrent [device](/docs/v1/start/glossary#device) limit. | 2 |
| license.payload.max_activations | integer or null | yes | See description | Cumulative activation cap; null means no configured cap. | 4 |
| license.payload.activation_count | integer | yes | See description | Cumulative consumed activations. | 1 |
| license.payload.min_version | string or null | yes | See description | Optional lower version restriction. | null |
| license.payload.max_version | string or null | yes | See description | Optional upper version restriction. | null |
| license.payload.active_devices | integer | yes | See description | Active count at snapshot time. | 1 |
| license.payload.customer | object | yes | See description | Customer information; personal data, not an authentication credential. | Nested object/array shown in full JSON |
| license.payload.customer.id | string | yes | See description | Customer public identifier. | "cus_d7db5516f8377f89a869e293" |
| license.payload.customer.name | string or null | yes | See description | Optional customer name. | "Synthetic contract customer" |
| license.payload.customer.email | string | yes | See description | Customer email associated with this license. | "contract-6b9f0843f1d3609f@example.invalid" |
| license.payload.customer.company | string or null | yes | See description | Optional company. | null |
| license.payload.customer.country | string or null | yes | See description | Optional country. | null |
| license.payload.product_details | object | yes | See description | Product display metadata. | Nested object/array shown in full JSON |
| license.payload.product_details.id | string | yes | See description | Public App ID. | "app_a642cd24641ec976626b" |
| license.payload.product_details.name | string | yes | See description | Product display name. | "Docs fixture product" |
| license.payload.product_details.platform | string | yes | See description | Lowercase configured product platform. | "desktop" |
| license.payload.plan_details | object | yes | See description | [Product plan](/docs/v1/start/glossary#product-plan), not the [developer](/docs/v1/start/glossary#developer) [platform subscription](/docs/v1/start/glossary#platform-subscription). | Nested object/array shown in full JSON |
| license.payload.plan_details.id | string | yes | See description | Internal product-plan identifier. | "plan_3bf54c099326639ee183661f" |
| license.payload.plan_details.name | string | yes | See description | Plan display name. | "Fixture plan" |
| license.payload.plan_details.slug | string | yes | See description | Plan slug. | "fixture-plan" |
| license.payload.plan_details.billing_type | string | yes | See description | Lowercase product-plan billing type. | "monthly" |
| license.payload.plan_details.price | number | yes | See description | PHP float; preserve numeric tokens such as19.0 for signatures. | 19.5 |
| license.payload.plan_details.currency | string | yes | See description | Configured product-plan currency. | "EUR" |
| license.payload.plan_details.duration_days | integer or null | yes | See description | Configured duration in days. | 30 |
| license.payload.plan_details.trial_days | integer or null | yes | See description | Configured trial duration in days. | null |
| license.payload.[features](/docs/v1/start/glossary#feature) | object or array | yes | See description | Dynamic plan feature JSON values; empty PHP feature array may be[]. Validate types before use. | Nested object/array shown in full JSON |
| license.payload.device | object | yes | See description | Installation binding and descriptions. | Nested object/array shown in full JSON |
| license.payload.device.id | string | yes | See description | Device public identifier. | "dev_299a6753c013047b3ed013de" |
| license.payload.device.installation_hash | string or null | yes | See description | SHA256 of supplied installation ID; null if validation header was omitted. | "e119b615ebfb5e5a2da5ea9c2a64b2117621399ff61d16718d460e0bb919a988" |
| license.payload.device.name | string or null | yes | See description | Optional device label; activation can return an empty string. | "Synthetic local installation" |
| license.payload.device.platform | string or null | yes | See description | Optional platform description. | "test" |
| license.payload.device.app_version | string or null | yes | See description | Optional application version. | "1.0.0" |
| license.payload.server_time | string | yes | format="date-time" | Signed snapshot time in UTC. | "2026-10-17T10:39:35+00:00" |
| license.payload.next_validation_at | string | yes | format="date-time" | server_time plus product interval hours. | "2026-10-18T10:39:35+00:00" |
| license.payload.offline_grace_until | string | yes | format="date-time" | server_time plus grace hours, not added after the [validation interval](/docs/v1/start/glossary#validation-interval). | "2026-10-24T10:39:35+00:00" |
| license.signature | string | yes | pattern="^[A-Za-z0-9_-]{86}$" | Unpadded base64url Ed25519 signature over canonical payload bytes. | "wYwFAPoyaaIVtq8zvUHlQeyqV91sI7115WJHUzY7unDEjOts7MrN_OL3PZAd-Ahd-OdfI-EjIwAVwhfLbYQlDQ" |
| license.algorithm | string | yes | const="Ed25519" | Fixed algorithm, not negotiated. | "Ed25519" |
| license.key_id | string | yes | See description | Must equal independently trusted key ID. | "ed25519_2b9bc7e214b1209f" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_11a6f2cf9cfffdaf92e1a6ce" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | No blind retry; apply the correction |
| INVALID_LICENSE | 404 | Activation key/product/environment/customer email mismatch; SDK empty key local error has no HTTP | Correct details | No blind retry; apply the correction |
| LICENSE_EXPIRED | 403 | Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTP | Renew eligible license | No blind retry; apply the correction |
| LICENSE_REVOKED | 403 | License REVOKED and no elapsed deadline taking precedence | Do not retry; contact developer | No blind retry; apply the correction |
| LICENSE_SUSPENDED | 403 | License SUSPENDED and no elapsed deadline taking precedence | Restore then reactivate | No blind retry; apply the correction |
| LICENSE_CANCELLED | 403 | License CANCELLED and no elapsed deadline taking precedence | Do not retry; new eligible license | No blind retry; apply the correction |
| LICENSE_PENDING | 403 | License PENDING and no elapsed deadline taking precedence | Wait for authorized activation | No blind retry; apply the correction |
| APP_VERSION_NOT_SUPPORTED | 403 | Nonempty activation app_version outside configured range | Use supported version | No blind retry; apply the correction |
| DEVICE_LIMIT_REACHED | 409 | New/inactive installation while active seats >= license.max_devices | Deactivate/reset a seat | No blind retry; apply the correction |
| ACTIVATION_LIMIT_REACHED | 409 | New/inactive installation while activation_count >= max_activations | Developer adjusts policy; reset-devices does not zero count | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only 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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Creates/reactivates a device and creates a new [activation token](/docs/v1/start/glossary#activation-token). A new or inactive device consumes activation_count and an active seat. Reusing an already ACTIVE device keeps the seat/count but revokes its earlier active tokens and issues a replacement token. Concurrent device checks and updates use a license transaction and row locks. Audit and [webhook](/docs/v1/start/glossary#webhook) license.activated are emitted; no email is sent directly. Device limits belong to the license, not the displayed platform-wide device allowance.
Authenticated requests can consume the [organization](/docs/v1/start/glossary#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. Repeating activation can rotate the token even when the device is unchanged. Save only the latest accepted token through your secure-storage flow. Do not run simultaneous activation requests for one installation; their responses can arrive out of order and leave one caller holding an already-revoked token.
## Rate limits
20 requests per60 seconds for SHA256(IP + Public App ID + supplied trimmed license-key text). Early invalid input can reject before this bucket is reached. This is not an organization-wide abuse quota.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh activate.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py activate.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs activate.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php activate.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- activate.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
Run node compiled/request.js activate.
### TypeScript
```typescript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: TypeScript7.0.2, @types/node22.19.7, Node18.20+ native fetch, trusted server/Electron main process.
// Run compiled request.js with public-key after strict TypeScript compilation. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations: Readonly> = Object.freeze({
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
});
function required(name: string): string {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2] ?? '';
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers: Record = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run java -cp .:gson.jar Request activate.
### Java
```java
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Java17+, Gson2.14.0. Run: java -cp .:gson.jar Request public-key
// Windows classpaths use semicolons. Expected: JSON; exit0 success/1 API error/2 setup or transport.
// Credentials/body: KDP_* environment variables. Private terminal only; output may contain secrets.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import com.google.gson.*;
public final class Request {
static final String CONTRACT = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""";
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) throw new IllegalArgumentException("Missing configuration.");
return value;
}
public static void main(String[] args) {
try {
JsonObject operations = JsonParser.parseString(CONTRACT).getAsJsonObject();
if (args.length != 1 || !operations.has(args[0])) throw new IllegalArgumentException("Unknown operation.");
String name = args[0]; JsonObject op = operations.getAsJsonObject(name);
String base = System.getenv().getOrDefault("KDP_BASE_URL", "https://licensing.kdpika.com/api/v1").replaceAll("/+$", "");
URI uri = URI.create(base);
boolean local = "http".equals(uri.getScheme()) && ("localhost".equals(uri.getHost()) || "127.0.0.1".equals(uri.getHost())) && "1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if (uri.getHost() == null || (!"https".equals(uri.getScheme()) && !local) || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null || !"/api/v1".equals(uri.getPath())) throw new IllegalArgumentException("Invalid base URL.");
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(base + op.get("path").getAsString())).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Java");
String auth = op.get("auth").getAsString();
if (auth.equals("publicApp")) request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX");
if (auth.equals("activation")) request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"));
if (name.equals("validate")) request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"));
if (op.get("method").getAsString().equals("POST")) {
String body = System.getenv().getOrDefault("KDP_BODY_JSON", "{}");
if (!JsonParser.parseString(body).isJsonObject()) throw new IllegalArgumentException("JSON object required.");
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else request.GET();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
System.out.write(response.body().getBytes(StandardCharsets.UTF_8)); System.out.write(10);
if (response.statusCode() < 200 || response.statusCode() >= 300 || !result.has("success") || !result.get("success").equals(new JsonPrimitive(true))) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); System.exit(1);
}
} catch (Exception error) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); System.exit(2);
}
}
}
```
Run go run request.go activate.
### Go
```go
// Purpose: inspect one client-callable Sandbox operation, not grant access.
// Requires Go1.20+ standard library. Run: go run request.go public-key
// Expected: JSON, exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables; output may contain secrets. Private terminal only.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type operation struct {
Method string `json:"method"`
Path string `json:"path"`
Auth string `json:"auth"`
}
const contract = `{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}`
func required(name string) string {
v := os.Getenv(name)
if v == "" || strings.ContainsAny(v, "\r\n") {
panic("configuration")
}
return v
}
func execute() int {
var operations map[string]operation
if json.Unmarshal([]byte(contract), &operations) != nil || len(os.Args) != 2 {
panic("operation")
}
name := os.Args[1]
op, ok := operations[name]
if !ok {
panic("operation")
}
base := os.Getenv("KDP_BASE_URL")
if base == "" {
base = "https://licensing.kdpika.com/api/v1"
}
base = strings.TrimRight(base, "/")
u, err := url.Parse(base)
if err != nil {
panic("URL")
}
local := u.Scheme == "http" && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") && os.Getenv("KDP_ALLOW_LOOPBACK") == "1"
if u.Hostname() == "" || (u.Scheme != "https" && !local) || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/api/v1" {
panic("URL")
}
body := ""
if op.Method == "POST" {
body = os.Getenv("KDP_BODY_JSON")
if body == "" {
body = "{}"
}
var object map[string]json.RawMessage
if json.Unmarshal([]byte(body), &object) != nil || object == nil {
panic("body")
}
}
request, err := http.NewRequest(op.Method, base+op.Path, strings.NewReader(body))
if err != nil {
panic("request")
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "KDPikaDocsExample/1.0 Go")
if op.Method == "POST" {
request.Header.Set("Content-Type", "application/json")
}
if op.Auth == "publicApp" {
request.Header.Set("X-App-ID", required("KDP_APP_ID"))
request.Header.Set("X-Environment", "SANDBOX")
}
if op.Auth == "activation" {
request.Header.Set("Authorization", "Bearer "+required("KDP_ACTIVATION_TOKEN"))
}
if name == "validate" {
request.Header.Set("X-Installation-ID", required("KDP_INSTALLATION_ID"))
}
client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
response, err := client.Do(request)
if err != nil {
panic("transport")
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 1048577))
if err != nil || len(raw) > 1048576 {
panic("response")
}
var result struct {
Success bool `json:"success"`
}
if json.Unmarshal(raw, &result) != nil {
panic("JSON")
}
fmt.Println(string(raw))
if response.StatusCode < 200 || response.StatusCode >= 300 || !result.Success {
fmt.Fprintln(os.Stderr, "API rejection. Inspect error.code/request_id; reconcile mutations before retrying.")
return 1
}
return 0
}
func main() {
code := 2
func() {
defer func() {
if recover() != nil {
fmt.Fprintln(os.Stderr, "Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.")
}
}()
code = execute()
}()
os.Exit(code)
}
```
Run cargo run --locked --manifest-path rust/Cargo.toml -- activate.
### Rust
```rust
// Purpose: inspect one client-callable Sandbox operation; not entitlement verification.
// Requires Rust1.82+, pinned Cargo.toml/Cargo.lock. Run: cargo run -- public-key
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, output may contain secrets.
use serde_json::Value;
use std::{env, process, time::Duration};
const CONTRACT: &str = r#"{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}"#;
fn required(name: &str) -> Result> {
let value = env::var(name)?;
if value.is_empty() || value.contains('\r') || value.contains('\n') {
return Err("configuration".into());
};
Ok(value)
}
fn execute() -> Result> {
let operations: Value = serde_json::from_str(CONTRACT)?;
let args: Vec = env::args().collect();
if args.len() != 2 {
return Err("operation".into());
}
let name = &args[1];
let op = operations.get(name).ok_or("operation")?;
let base = env::var("KDP_BASE_URL")
.unwrap_or_else(|_| "https://licensing.kdpika.com/api/v1".into())
.trim_end_matches('/')
.to_string();
let uri = url::Url::parse(&base)?;
let local = uri.scheme() == "http"
&& matches!(uri.host_str(), Some("127.0.0.1" | "localhost"))
&& env::var("KDP_ALLOW_LOOPBACK").unwrap_or_default() == "1";
if uri.host_str().is_none()
|| (uri.scheme() != "https" && !local)
|| !uri.username().is_empty()
|| uri.password().is_some()
|| uri.query().is_some()
|| uri.fragment().is_some()
|| uri.path() != "/api/v1"
{
return Err("URL".into());
}
let method = op["method"].as_str().ok_or("method")?;
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.redirects(0)
.build();
let target = base + op["path"].as_str().ok_or("path")?;
let mut request = agent
.request(method, &target)
.set("Accept", "application/json")
.set("User-Agent", "KDPikaDocsExample/1.0 Rust");
if op["auth"] == "publicApp" {
request = request
.set("X-App-ID", &required("KDP_APP_ID")?)
.set("X-Environment", "SANDBOX")
}
if op["auth"] == "activation" {
request = request.set(
"Authorization",
&("Bearer ".to_string() + &required("KDP_ACTIVATION_TOKEN")?),
)
}
if name == "validate" {
request = request.set("X-Installation-ID", &required("KDP_INSTALLATION_ID")?)
}
let answer = if method == "POST" {
let body = env::var("KDP_BODY_JSON").unwrap_or_else(|_| "{}".into());
if !serde_json::from_str::(&body)?.is_object() {
return Err("body".into());
}
request
.set("Content-Type", "application/json")
.send_string(&body)
} else {
request.call()
};
let response = match answer {
Ok(response) => response,
Err(ureq::Error::Status(_, response)) => response,
Err(error) => return Err(Box::new(error)),
};
let status = response.status();
let raw = response.into_string()?;
let result: Value = serde_json::from_str(&raw)?;
println!("{}", raw);
if !(200..300).contains(&status) || result["success"] != true {
eprintln!(
"API rejection. Inspect error.code/request_id; reconcile mutations before retrying."
);
return Ok(1);
}
Ok(0)
}
fn main() {
process::exit(match execute() {
Ok(code) => code,
Err(_) => {
eprintln!("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.");
2
}
})
}
```
```rust
[package]
name = "kdpika-api-example"
version = "1.0.0"
edition = "2021"
rust-version = "1.82"
[dependencies]
ureq = { version = "=2.12.1", default-features = false, features = ["tls"] }
url = "=2.5.2"
serde_json = "=1.0.145"
# Keep the tested Rust1.82 toolchain compatible with this pinned lock.
zeroize = "=1.8.1"
```
Run ruby request.rb activate.
### Ruby
```ruby
# Purpose: inspect one client-callable Sandbox operation, not grant feature access.
# Requires Ruby2.6+ standard library. Run: ruby request.rb public-key
# Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
# Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
STDOUT.set_encoding(Encoding::UTF_8)
OPERATIONS = JSON.parse(<<~'JSON')
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
JSON
def required(name)
value = ENV.fetch(name, '')
raise 'configuration' if value.empty? || value.match?(/[\r\n]/)
value
end
begin
raise 'operation' unless ARGV.length == 1 && OPERATIONS.key?(ARGV[0])
name = ARGV[0]; op = OPERATIONS.fetch(name)
base = ENV.fetch('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').sub(%r{/+$}, '')
uri = URI.parse(base)
local = uri.scheme == 'http' && ['127.0.0.1','localhost'].include?(uri.host) && ENV['KDP_ALLOW_LOOPBACK'] == '1'
raise 'URL' unless uri.host && (uri.scheme == 'https' || local) && !uri.userinfo && !uri.query && !uri.fragment && uri.path == '/api/v1'
target = URI.parse(base + op.fetch('path'))
request = op.fetch('method') == 'GET' ? Net::HTTP::Get.new(target) : Net::HTTP::Post.new(target)
request['Accept'] = 'application/json'; request['User-Agent'] = 'KDPikaDocsExample/1.0 Ruby'
if op['auth'] == 'publicApp'
request['X-App-ID'] = required('KDP_APP_ID'); request['X-Environment'] = 'SANDBOX'
elsif op['auth'] == 'activation'
request['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
end
request['X-Installation-ID'] = required('KDP_INSTALLATION_ID') if name == 'validate'
if op['method'] == 'POST'
body = ENV.fetch('KDP_BODY_JSON', '{}'); raise 'JSON object' unless JSON.parse(body).is_a?(Hash)
request['Content-Type'] = 'application/json'; request.body = body
end
http = Net::HTTP.new(target.host, target.port, nil)
http.use_ssl = target.scheme == 'https'; http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 5; http.read_timeout = 15
response = http.request(request) # Net::HTTP does not follow redirects.
result = JSON.parse(response.body)
puts response.body
unless response.code.to_i.between?(200,299) && result['success'] == true
warn 'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.'; exit 1
end
rescue StandardError
warn 'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.'; exit 2
end
```
Run java -cp request.jar:gson.jar RequestKt activate.
### Kotlin
```kotlin
// Purpose: inspect one client-callable Sandbox operation; not an entitlement verifier.
// Requires Kotlin2.4.21/JVM17+, Gson2.14.0. Run compiled RequestKt with public-key.
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, never application/CI logs.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import kotlin.system.exitProcess
val operationsJson = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""".trimIndent()
fun required(name: String): String = (System.getenv(name) ?: "").also {
require(it.isNotEmpty() && !it.contains('\r') && !it.contains('\n')) { "Missing configuration." }
}
fun main(args: Array) {
try {
val operations = JsonParser.parseString(operationsJson).asJsonObject
require(args.size == 1 && operations.has(args[0]))
val name = args[0]; val op = operations.getAsJsonObject(name)
val base = (System.getenv("KDP_BASE_URL") ?: "https://licensing.kdpika.com/api/v1").trimEnd('/')
val uri = URI.create(base)
val local = uri.scheme == "http" && uri.host in listOf("localhost", "127.0.0.1") && System.getenv("KDP_ALLOW_LOOPBACK") == "1"
require(uri.host != null && (uri.scheme == "https" || local) && uri.userInfo == null && uri.query == null && uri.fragment == null && uri.path == "/api/v1")
val request = HttpRequest.newBuilder(URI.create(base + op["path"].asString)).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Kotlin")
when (op["auth"].asString) {
"publicApp" -> request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX")
"activation" -> request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"))
}
if (name == "validate") request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"))
if (op["method"].asString == "POST") {
val body = System.getenv("KDP_BODY_JSON") ?: "{}"
require(JsonParser.parseString(body).isJsonObject)
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body))
} else request.GET()
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build()
val response = client.send(request.build(), HttpResponse.BodyHandlers.ofString())
val result = JsonParser.parseString(response.body()).asJsonObject
System.out.write(response.body().toByteArray(Charsets.UTF_8)); System.out.write(10)
if (response.statusCode() !in 200..299 || result["success"] != JsonPrimitive(true)) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); exitProcess(1)
}
} catch (error: Exception) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); exitProcess(2)
}
}
```
Run ./request activate.
### Swift
```swift
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Swift5.7+, Foundation; FoundationNetworking on Linux.
// Build: swiftc -parse-as-library request.swift -o request; run: ./request public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let contract = #"""
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
"""#
enum SetupError: Error {case invalid}
func required(_ name:String) throws -> String {
let value=ProcessInfo.processInfo.environment[name] ?? ""
guard !value.isEmpty && !value.contains("\r") && !value.contains("\n") else {throw SetupError.invalid};return value
}
final class NoRedirect: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(_ session:URLSession,task:URLSessionTask,willPerformHTTPRedirection response:HTTPURLResponse,newRequest request:URLRequest,completionHandler:@escaping(URLRequest?)->Void){completionHandler(nil)}
}
@main struct Example {
static func main() async {
do {
let env=ProcessInfo.processInfo.environment
let operations=try JSONSerialization.jsonObject(with:Data(contract.utf8)) as! [String:[String:String]]
guard CommandLine.arguments.count==2,let op=operations[CommandLine.arguments[1]] else {throw SetupError.invalid}
let name=CommandLine.arguments[1]
let base=(env["KDP_BASE_URL"] ?? "https://licensing.kdpika.com/api/v1").replacingOccurrences(of:"/+$",with:"",options:.regularExpression)
guard let parts=URLComponents(string:base),let host=parts.host else {throw SetupError.invalid}
let local=parts.scheme=="http" && ["localhost","127.0.0.1"].contains(host) && env["KDP_ALLOW_LOOPBACK"]=="1"
guard (parts.scheme=="https" || local) && parts.user==nil && parts.password==nil && parts.query==nil && parts.fragment==nil && parts.path=="/api/v1",let url=URL(string:base+op["path"]!) else {throw SetupError.invalid}
var request=URLRequest(url:url);request.httpMethod=op["method"];request.timeoutInterval=15
request.setValue("application/json",forHTTPHeaderField:"Accept");request.setValue("KDPikaDocsExample/1.0 Swift",forHTTPHeaderField:"User-Agent")
if op["auth"]=="publicApp" {request.setValue(try required("KDP_APP_ID"),forHTTPHeaderField:"X-App-ID");request.setValue("SANDBOX",forHTTPHeaderField:"X-Environment")}
if op["auth"]=="activation" {request.setValue("Bearer "+(try required("KDP_ACTIVATION_TOKEN")),forHTTPHeaderField:"Authorization")}
if name=="validate" {request.setValue(try required("KDP_INSTALLATION_ID"),forHTTPHeaderField:"X-Installation-ID")}
if op["method"]=="POST" {
let body=Data((env["KDP_BODY_JSON"] ?? "{}").utf8)
guard try JSONSerialization.jsonObject(with:body) is [String:Any] else {throw SetupError.invalid}
request.httpBody=body;request.setValue("application/json",forHTTPHeaderField:"Content-Type")
}
let session=URLSession(configuration:.ephemeral,delegate:NoRedirect(),delegateQueue:nil)
defer {session.invalidateAndCancel()}
let (raw,response)=try await session.data(for:request)
guard let http=response as? HTTPURLResponse,let result=try JSONSerialization.jsonObject(with:raw) as? [String:Any],let text=String(data:raw,encoding:.utf8) else {throw SetupError.invalid}
_ = text // Decoding above checks UTF-8 without reserializing JSON.
FileHandle.standardOutput.write(raw);FileHandle.standardOutput.write(Data([10]))
guard (200..<300).contains(http.statusCode),result["success"] as? Bool == true else {
FileHandle.standardError.write(Data("API rejection. Inspect error.code/request_id; reconcile mutations before retrying.\n".utf8));exit(1)
}
} catch {
FileHandle.standardError.write(Data("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n".utf8));exit(2)
}
}
}
```
Run dart run request.dart activate.
### Dart
```dart
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Dart3.7+ standard library. Run: dart run request.dart public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body. Private terminal only; output can contain secrets.
import 'dart:convert';
import 'dart:io';
const contract = r'''{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}''';
String required(String name) {
final value = Platform.environment[name] ?? '';
if (value.isEmpty || value.contains('\r') || value.contains('\n'))
throw StateError('configuration');
return value;
}
Future main(List args) async {
stdout.encoding = utf8;
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
try {
final operations = jsonDecode(contract) as Map;
if (args.length != 1 || !operations.containsKey(args[0]))
throw StateError('operation');
final name = args[0];
final op = operations[name] as Map;
final base =
(Platform.environment['KDP_BASE_URL'] ??
'https://licensing.kdpika.com/api/v1')
.replaceFirst(RegExp(r'/+$'), '');
final uri = Uri.parse(base);
final local =
uri.scheme == 'http' &&
['127.0.0.1', 'localhost'].contains(uri.host) &&
Platform.environment['KDP_ALLOW_LOOPBACK'] == '1';
if (uri.host.isEmpty ||
(uri.scheme != 'https' && !local) ||
uri.userInfo.isNotEmpty ||
uri.hasQuery ||
uri.hasFragment ||
uri.path != '/api/v1')
throw StateError('URL');
final request = await client
.openUrl(
op['method'] as String,
Uri.parse(base + (op['path'] as String)),
)
.timeout(const Duration(seconds: 15));
request.followRedirects = false;
request.headers.set('Accept', 'application/json');
request.headers.set('User-Agent', 'KDPikaDocsExample/1.0 Dart');
if (op['auth'] == 'publicApp') {
request.headers.set('X-App-ID', required('KDP_APP_ID'));
request.headers.set('X-Environment', 'SANDBOX');
}
if (op['auth'] == 'activation')
request.headers.set(
'Authorization',
'Bearer ${required('KDP_ACTIVATION_TOKEN')}',
);
if (name == 'validate')
request.headers.set('X-Installation-ID', required('KDP_INSTALLATION_ID'));
if (op['method'] == 'POST') {
final body = Platform.environment['KDP_BODY_JSON'] ?? '{}';
if (jsonDecode(body) is! Map) throw StateError('JSON object');
request.headers.set('Content-Type', 'application/json');
request.add(utf8.encode(body));
}
final response = await request.close().timeout(const Duration(seconds: 15));
final raw = await utf8.decoder
.bind(response)
.join()
.timeout(const Duration(seconds: 15));
final result = jsonDecode(raw) as Map;
stdout.writeln(raw);
if (response.statusCode < 200 ||
response.statusCode >= 300 ||
result['success'] != true) {
stderr.writeln(
'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.',
);
exitCode = 1;
}
} catch (error) {
stderr.writeln(
'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.',
);
exitCode = 2;
} finally {
client.close(force: true);
}
}
```
Run ./request-c activate.
### C
```c
/* C11 entry point. Build with client_http.c include, yyjson0.13.0 and libcurl7.76+.
* Run request public-key; expected JSON, exit0 success/1 rejection/2 unavailable. */
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```c
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
Run ./request-cpp activate.
### C++
```cpp
// C++17 entry point using the complete adjacent native transport.
// Requires yyjson0.13.0 and libcurl7.76+. Run request public-key; JSON/exit0,1,2.
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```cpp
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); 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
| Symptom | Cause | Action |
| --- | --- | --- |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
| INVALID_REQUEST (HTTP 422) | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry |
| INVALID_LICENSE (HTTP 404) | Activation key/product/environment/customer email mismatch; SDK empty key local error has no HTTP | Correct details |
| LICENSE_EXPIRED (HTTP 403) | Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTP | Renew eligible license |
| LICENSE_REVOKED (HTTP 403) | License REVOKED and no elapsed deadline taking precedence | Do not retry; contact developer |
| LICENSE_SUSPENDED (HTTP 403) | License SUSPENDED and no elapsed deadline taking precedence | Restore then reactivate |
| LICENSE_CANCELLED (HTTP 403) | License CANCELLED and no elapsed deadline taking precedence | Do not retry; new eligible license |
| LICENSE_PENDING (HTTP 403) | License PENDING and no elapsed deadline taking precedence | Wait for authorized activation |
| APP_VERSION_NOT_SUPPORTED (HTTP 403) | Nonempty activation app_version outside configured range | Use supported version |
| DEVICE_LIMIT_REACHED (HTTP 409) | New/inactive installation while active seats >= license.max_devices | Deactivate/reset a seat |
| ACTIVATION_LIMIT_REACHED (HTTP 409) | New/inactive installation while activation_count >= max_activations | Developer adjusts policy; reset-devices does not zero count |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Test the complete lifecycle
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/test
---
slug: "/docs/v1/integration-guide/test"
title: "Test the complete lifecycle"
description: "Stage 13: test the complete lifecycle with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 28
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Exercise negative cases before [customers](/docs/v1/start/glossary#customer) rely on your integration.
Use [Sandbox](/docs/v1/start/glossary#sandbox) only. This is a checklist for your implementation, not permission to test attacks on other systems.
1. Run the Stage 7 file with activate. Confirm one active [device](/docs/v1/start/glossary#device).
2. Run validate, then check after restarting the process. Confirm the same identity/seat.
3. Activate on a second private state directory. With Max devices 2, a third installation must return MAX_DEVICES_REACHED.
4. Run deactivate on one installation. A replacement [activation](/docs/v1/start/glossary#activation) should now fit.
5. In **[Licenses](/docs/v1/start/glossary#license) → Manage**, choose Suspend. A forced online validate must deny. Restore allows a new activation; it does not revive the old revoked token. Run activate again before expecting validation to succeed.
6. Create a separate test license, choose Revoke and verify denial. Revocation is not a temporary support toggle.
7. Test a short-duration/expired license. Do not alter the server clock or production database to fake this result.
8. Test supported renewal through the documented backend/store workflow, with a confirmed paid period; it must not duplicate the license. Renew extends the license, not the token's original lifetime. Validate online to obtain the updated signed decision; activate again if the existing token has expired or was revoked.
9. Disconnect the network; test valid, missing, corrupted and expired cache states.
10. Modify a signed payload in an isolated verifier test. The verifier must deny it.
11. Test wrong email, App ID and environment. Cross-environment access must fail.
12. Follow your selected store's duplicate, failure, cancellation and refund checklist.
:::tabs
:::tab python Python
Use the exact same file and state directory for a lifecycle:
```bash
.venv/bin/python license-check.py activate
.venv/bin/python license-check.py validate
.venv/bin/python license-check.py check
.venv/bin/python license-check.py deactivate
```
Activation/validation success prints Sandbox license verified.; deactivation prints Device released. A failure exits nonzero and must not unlock your software.
:::
:::tab javascript JavaScript / Node.js
Use the exact same file and state directory for a lifecycle:
```bash
node license-check.mjs activate
node license-check.mjs validate
node license-check.mjs check
node license-check.mjs deactivate
```
Activation/validation success prints Sandbox license verified.; deactivation prints Device released. A failure exits nonzero and must not unlock your software.
:::
:::tab php PHP
Use the exact same file and state directory for a lifecycle:
```bash
php license-check.php activate
php license-check.php validate
php license-check.php check
php license-check.php deactivate
```
Activation/validation success prints Sandbox license verified.; deactivation prints Device released. A failure exits nonzero and must not unlock your software.
:::
:::tab csharp C# / .NET
Use the exact same file and state directory for a lifecycle:
```bash
dotnet run --project Guide.csproj -- activate
dotnet run --project Guide.csproj -- validate
dotnet run --project Guide.csproj -- check
dotnet run --project Guide.csproj -- deactivate
```
Activation/validation success prints Sandbox license verified.; deactivation prints Device released. A failure exits nonzero and must not unlock your software.
:::
:::tab curl cURL / other language
Use the same activate → validate → deactivate sequence in your real REST client project and its signature verifier. Exercise every negative case above; a cURL HTTP response alone is not a passing access check.
Use the selected language SDK or REST client from [SDKs and languages](/docs/v1/sdks/overview), its [signature verifier](/docs/v1/security/verify-ed25519), and the supported [webhook verifier](/docs/v1/guides/verify-webhook-signatures).
:::
:::
## Expected result
Your test notes show both successful access and deliberate denials, without real charges or leaked tokens.
## If it does not work
Stop at the first unsafe result. Keep the application locked, record a safe [request ID](/docs/v1/start/glossary#request-id) and contact support. Do not “fix” a negative test by removing its signature or entitlement guard.
## Stage checklist
- All lifecycle, device, state, network and signature cases pass for my runtime.
- I tested the non-programmer [delivery](/docs/v1/start/glossary#delivery) route if I use one.
## Next steps
[Continue to Stage 14: Go live safely](/docs/v1/integration-guide/go-live)
# Validate an installation
Canonical: https://licensing.kdpika.com/docs/v1/api/validate
---
title: "Validate an installation"
description: "Refresh the signed state of an activated installation."
slug: "/docs/v1/api/validate"
section: "API reference"
order: 29
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
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
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/[licenses](/docs/v1/start/glossary#license)/validate
### When to use it
Refresh the signed state of an activated installation. Send its [activation token](/docs/v1/start/glossary#activation-token) and the exact persisted [installation ID](/docs/v1/start/glossary#installation-id), then verify the returned envelope and semantic policy before enabling [features](/docs/v1/start/glossary#feature).
## Authentication and environment
Use Authorization: Bearer YOUR_ACTIVATION_TOKEN. The token fixes the installation/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Installation token | Bearer YOUR_ACTIVATION_TOKEN |
| X-Installation-ID | no | Always 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
```json
{}
```
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](/docs/v1/api/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.
```json
{
"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
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| valid | boolean | yes | const=true | Server accepted this validation; still verify the signed snapshot. | true |
| license | object | yes | See description | Only payload is signed. This is PHP-specific canonical JSON, not RFC8785. | Nested object/array shown in full JSON |
| license.payload | object | yes | See description | Exact signed snapshot. Signature authenticity alone does not grant access. | Nested object/array shown in full JSON |
| license.payload.license_id | string | yes | See description | License public identifier. | "lic_578581bd80c2a672da30fc29" |
| license.payload.product_id | string | yes | See description | [Public App ID](/docs/v1/start/glossary#public-app-id); compare to configured product. | "app_a642cd24641ec976626b" |
| license.payload.product | string | yes | See description | Product display name. | "Docs fixture product" |
| license.payload.plan | string | yes | See description | Product-plan slug. | "fixture-plan" |
| license.payload.status | string | yes | enum=["active", "suspended", "revoked", "expired", "pending", "cancelled"] | Stored license status in lowercase. | "active" |
| license.payload.environment | string | yes | enum=["sandbox", "[live](/docs/v1/start/glossary#live)"] | Bound license environment. | "sandbox" |
| license.payload.expires_at | string or null | yes | format="date-time" | Independent license expiry; null has no configured expiry. | "2026-11-16T10:39:35+00:00" |
| license.payload.license_key_mask | string | yes | See description | Display-only masked key. | "TESTDOCS-••••-••••-HAHH" |
| license.payload.starts_at | string or null | yes | format="date-time" | Configured license start, currently not enforced by server eligibility. | "2026-10-17T10:39:35+00:00" |
| license.payload.created_at | string or null | yes | format="date-time" | Creation timestamp; database-timezone discrepancy is disclosed. | "2026-10-17T12:39:35+00:00" |
| license.payload.max_devices | integer | yes | See description | Concurrent [device](/docs/v1/start/glossary#device) limit. | 2 |
| license.payload.max_activations | integer or null | yes | See description | Cumulative [activation](/docs/v1/start/glossary#activation) cap; null means no configured cap. | 4 |
| license.payload.activation_count | integer | yes | See description | Cumulative consumed activations. | 1 |
| license.payload.min_version | string or null | yes | See description | Optional lower version restriction. | null |
| license.payload.max_version | string or null | yes | See description | Optional upper version restriction. | null |
| license.payload.active_devices | integer | yes | See description | Active count at snapshot time. | 1 |
| license.payload.customer | object | yes | See description | Customer information; personal data, not an authentication credential. | Nested object/array shown in full JSON |
| license.payload.customer.id | string | yes | See description | Customer public identifier. | "cus_d7db5516f8377f89a869e293" |
| license.payload.customer.name | string or null | yes | See description | Optional customer name. | "Synthetic contract customer" |
| license.payload.customer.email | string | yes | See description | Customer email associated with this license. | "contract-6b9f0843f1d3609f@example.invalid" |
| license.payload.customer.company | string or null | yes | See description | Optional company. | null |
| license.payload.customer.country | string or null | yes | See description | Optional country. | null |
| license.payload.product_details | object | yes | See description | Product display metadata. | Nested object/array shown in full JSON |
| license.payload.product_details.id | string | yes | See description | Public App ID. | "app_a642cd24641ec976626b" |
| license.payload.product_details.name | string | yes | See description | Product display name. | "Docs fixture product" |
| license.payload.product_details.platform | string | yes | See description | Lowercase configured product platform. | "desktop" |
| license.payload.plan_details | object | yes | See description | [Product plan](/docs/v1/start/glossary#product-plan), not the [developer](/docs/v1/start/glossary#developer) [platform subscription](/docs/v1/start/glossary#platform-subscription). | Nested object/array shown in full JSON |
| license.payload.plan_details.id | string | yes | See description | Internal product-plan identifier. | "plan_3bf54c099326639ee183661f" |
| license.payload.plan_details.name | string | yes | See description | Plan display name. | "Fixture plan" |
| license.payload.plan_details.slug | string | yes | See description | Plan slug. | "fixture-plan" |
| license.payload.plan_details.billing_type | string | yes | See description | Lowercase product-plan billing type. | "monthly" |
| license.payload.plan_details.price | number | yes | See description | PHP float; preserve numeric tokens such as19.0 for signatures. | 19.5 |
| license.payload.plan_details.currency | string | yes | See description | Configured product-plan currency. | "EUR" |
| license.payload.plan_details.duration_days | integer or null | yes | See description | Configured duration in days. | 30 |
| license.payload.plan_details.trial_days | integer or null | yes | See description | Configured trial duration in days. | null |
| license.payload.features | object or array | yes | See description | Dynamic plan feature JSON values; empty PHP feature array may be[]. Validate types before use. | Nested object/array shown in full JSON |
| license.payload.device | object | yes | See description | Installation binding and descriptions. | Nested object/array shown in full JSON |
| license.payload.device.id | string | yes | See description | Device public identifier. | "dev_299a6753c013047b3ed013de" |
| license.payload.device.installation_hash | string or null | yes | See description | SHA256 of supplied installation ID; null if validation header was omitted. | "e119b615ebfb5e5a2da5ea9c2a64b2117621399ff61d16718d460e0bb919a988" |
| license.payload.device.name | string or null | yes | See description | Optional device label; activation can return an empty string. | "Synthetic local installation" |
| license.payload.device.platform | string or null | yes | See description | Optional platform description. | "test" |
| license.payload.device.app_version | string or null | yes | See description | Optional application version. | "1.0.0" |
| license.payload.server_time | string | yes | format="date-time" | Signed snapshot time in UTC. | "2026-10-17T10:39:35+00:00" |
| license.payload.next_validation_at | string | yes | format="date-time" | server_time plus product interval hours. | "2026-10-18T10:39:35+00:00" |
| license.payload.offline_grace_until | string | yes | format="date-time" | server_time plus grace hours, not added after the [validation interval](/docs/v1/start/glossary#validation-interval). | "2026-10-24T10:39:35+00:00" |
| license.signature | string | yes | pattern="^[A-Za-z0-9_-]{86}$" | Unpadded base64url Ed25519 signature over canonical payload bytes. | "wYwFAPoyaaIVtq8zvUHlQeyqV91sI7115WJHUzY7unDEjOts7MrN_OL3PZAd-Ahd-OdfI-EjIwAVwhfLbYQlDQ" |
| license.algorithm | string | yes | const="Ed25519" | Fixed algorithm, not negotiated. | "Ed25519" |
| license.key_id | string | yes | See description | Must equal independently trusted key ID. | "ed25519_2b9bc7e214b1209f" |
| request_id | string | yes | pattern="^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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
| INVALID_TOKEN | 401 | Missing/non-act_/unknown/revoked/deactivated activation token | Reactivate eligible installation | No blind retry; apply the correction |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | No blind retry; apply the correction |
| DEVICE_NOT_AUTHORIZED | 403 | Token is active but associated device is not ACTIVE | Reactivation if eligible | No blind retry; apply the correction |
| DEVICE_MISMATCH | 403 | Provided installation ID does not match token-bound HMAC hash | Use original installation; do not copy token | No blind retry; apply the correction |
| LICENSE_EXPIRED | 403 | Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTP | Renew eligible license | No blind retry; apply the correction |
| LICENSE_REVOKED | 403 | License REVOKED and no elapsed deadline taking precedence | Do not retry; contact developer | No blind retry; apply the correction |
| LICENSE_SUSPENDED | 403 | License SUSPENDED and no elapsed deadline taking precedence | Restore then reactivate | No blind retry; apply the correction |
| LICENSE_CANCELLED | 403 | License CANCELLED and no elapsed deadline taking precedence | Do not retry; new eligible license | No blind retry; apply the correction |
| LICENSE_PENDING | 403 | License PENDING and no elapsed deadline taking precedence | Wait for authorized activation | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only 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](/docs/v1/api/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](/docs/v1/start/glossary#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](/docs/v1/start/glossary#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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh validate.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py validate.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs validate.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php validate.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- validate.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
Run node compiled/request.js validate.
### TypeScript
```typescript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: TypeScript7.0.2, @types/node22.19.7, Node18.20+ native fetch, trusted server/Electron main process.
// Run compiled request.js with public-key after strict TypeScript compilation. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations: Readonly> = Object.freeze({
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
});
function required(name: string): string {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2] ?? '';
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers: Record = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run java -cp .:gson.jar Request validate.
### Java
```java
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Java17+, Gson2.14.0. Run: java -cp .:gson.jar Request public-key
// Windows classpaths use semicolons. Expected: JSON; exit0 success/1 API error/2 setup or transport.
// Credentials/body: KDP_* environment variables. Private terminal only; output may contain secrets.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import com.google.gson.*;
public final class Request {
static final String CONTRACT = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""";
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) throw new IllegalArgumentException("Missing configuration.");
return value;
}
public static void main(String[] args) {
try {
JsonObject operations = JsonParser.parseString(CONTRACT).getAsJsonObject();
if (args.length != 1 || !operations.has(args[0])) throw new IllegalArgumentException("Unknown operation.");
String name = args[0]; JsonObject op = operations.getAsJsonObject(name);
String base = System.getenv().getOrDefault("KDP_BASE_URL", "https://licensing.kdpika.com/api/v1").replaceAll("/+$", "");
URI uri = URI.create(base);
boolean local = "http".equals(uri.getScheme()) && ("localhost".equals(uri.getHost()) || "127.0.0.1".equals(uri.getHost())) && "1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if (uri.getHost() == null || (!"https".equals(uri.getScheme()) && !local) || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null || !"/api/v1".equals(uri.getPath())) throw new IllegalArgumentException("Invalid base URL.");
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(base + op.get("path").getAsString())).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Java");
String auth = op.get("auth").getAsString();
if (auth.equals("publicApp")) request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX");
if (auth.equals("activation")) request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"));
if (name.equals("validate")) request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"));
if (op.get("method").getAsString().equals("POST")) {
String body = System.getenv().getOrDefault("KDP_BODY_JSON", "{}");
if (!JsonParser.parseString(body).isJsonObject()) throw new IllegalArgumentException("JSON object required.");
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else request.GET();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
System.out.write(response.body().getBytes(StandardCharsets.UTF_8)); System.out.write(10);
if (response.statusCode() < 200 || response.statusCode() >= 300 || !result.has("success") || !result.get("success").equals(new JsonPrimitive(true))) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); System.exit(1);
}
} catch (Exception error) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); System.exit(2);
}
}
}
```
Run go run request.go validate.
### Go
```go
// Purpose: inspect one client-callable Sandbox operation, not grant access.
// Requires Go1.20+ standard library. Run: go run request.go public-key
// Expected: JSON, exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables; output may contain secrets. Private terminal only.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type operation struct {
Method string `json:"method"`
Path string `json:"path"`
Auth string `json:"auth"`
}
const contract = `{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}`
func required(name string) string {
v := os.Getenv(name)
if v == "" || strings.ContainsAny(v, "\r\n") {
panic("configuration")
}
return v
}
func execute() int {
var operations map[string]operation
if json.Unmarshal([]byte(contract), &operations) != nil || len(os.Args) != 2 {
panic("operation")
}
name := os.Args[1]
op, ok := operations[name]
if !ok {
panic("operation")
}
base := os.Getenv("KDP_BASE_URL")
if base == "" {
base = "https://licensing.kdpika.com/api/v1"
}
base = strings.TrimRight(base, "/")
u, err := url.Parse(base)
if err != nil {
panic("URL")
}
local := u.Scheme == "http" && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") && os.Getenv("KDP_ALLOW_LOOPBACK") == "1"
if u.Hostname() == "" || (u.Scheme != "https" && !local) || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/api/v1" {
panic("URL")
}
body := ""
if op.Method == "POST" {
body = os.Getenv("KDP_BODY_JSON")
if body == "" {
body = "{}"
}
var object map[string]json.RawMessage
if json.Unmarshal([]byte(body), &object) != nil || object == nil {
panic("body")
}
}
request, err := http.NewRequest(op.Method, base+op.Path, strings.NewReader(body))
if err != nil {
panic("request")
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "KDPikaDocsExample/1.0 Go")
if op.Method == "POST" {
request.Header.Set("Content-Type", "application/json")
}
if op.Auth == "publicApp" {
request.Header.Set("X-App-ID", required("KDP_APP_ID"))
request.Header.Set("X-Environment", "SANDBOX")
}
if op.Auth == "activation" {
request.Header.Set("Authorization", "Bearer "+required("KDP_ACTIVATION_TOKEN"))
}
if name == "validate" {
request.Header.Set("X-Installation-ID", required("KDP_INSTALLATION_ID"))
}
client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
response, err := client.Do(request)
if err != nil {
panic("transport")
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 1048577))
if err != nil || len(raw) > 1048576 {
panic("response")
}
var result struct {
Success bool `json:"success"`
}
if json.Unmarshal(raw, &result) != nil {
panic("JSON")
}
fmt.Println(string(raw))
if response.StatusCode < 200 || response.StatusCode >= 300 || !result.Success {
fmt.Fprintln(os.Stderr, "API rejection. Inspect error.code/request_id; reconcile mutations before retrying.")
return 1
}
return 0
}
func main() {
code := 2
func() {
defer func() {
if recover() != nil {
fmt.Fprintln(os.Stderr, "Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.")
}
}()
code = execute()
}()
os.Exit(code)
}
```
Run cargo run --locked --manifest-path rust/Cargo.toml -- validate.
### Rust
```rust
// Purpose: inspect one client-callable Sandbox operation; not entitlement verification.
// Requires Rust1.82+, pinned Cargo.toml/Cargo.lock. Run: cargo run -- public-key
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, output may contain secrets.
use serde_json::Value;
use std::{env, process, time::Duration};
const CONTRACT: &str = r#"{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}"#;
fn required(name: &str) -> Result> {
let value = env::var(name)?;
if value.is_empty() || value.contains('\r') || value.contains('\n') {
return Err("configuration".into());
};
Ok(value)
}
fn execute() -> Result> {
let operations: Value = serde_json::from_str(CONTRACT)?;
let args: Vec = env::args().collect();
if args.len() != 2 {
return Err("operation".into());
}
let name = &args[1];
let op = operations.get(name).ok_or("operation")?;
let base = env::var("KDP_BASE_URL")
.unwrap_or_else(|_| "https://licensing.kdpika.com/api/v1".into())
.trim_end_matches('/')
.to_string();
let uri = url::Url::parse(&base)?;
let local = uri.scheme() == "http"
&& matches!(uri.host_str(), Some("127.0.0.1" | "localhost"))
&& env::var("KDP_ALLOW_LOOPBACK").unwrap_or_default() == "1";
if uri.host_str().is_none()
|| (uri.scheme() != "https" && !local)
|| !uri.username().is_empty()
|| uri.password().is_some()
|| uri.query().is_some()
|| uri.fragment().is_some()
|| uri.path() != "/api/v1"
{
return Err("URL".into());
}
let method = op["method"].as_str().ok_or("method")?;
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.redirects(0)
.build();
let target = base + op["path"].as_str().ok_or("path")?;
let mut request = agent
.request(method, &target)
.set("Accept", "application/json")
.set("User-Agent", "KDPikaDocsExample/1.0 Rust");
if op["auth"] == "publicApp" {
request = request
.set("X-App-ID", &required("KDP_APP_ID")?)
.set("X-Environment", "SANDBOX")
}
if op["auth"] == "activation" {
request = request.set(
"Authorization",
&("Bearer ".to_string() + &required("KDP_ACTIVATION_TOKEN")?),
)
}
if name == "validate" {
request = request.set("X-Installation-ID", &required("KDP_INSTALLATION_ID")?)
}
let answer = if method == "POST" {
let body = env::var("KDP_BODY_JSON").unwrap_or_else(|_| "{}".into());
if !serde_json::from_str::(&body)?.is_object() {
return Err("body".into());
}
request
.set("Content-Type", "application/json")
.send_string(&body)
} else {
request.call()
};
let response = match answer {
Ok(response) => response,
Err(ureq::Error::Status(_, response)) => response,
Err(error) => return Err(Box::new(error)),
};
let status = response.status();
let raw = response.into_string()?;
let result: Value = serde_json::from_str(&raw)?;
println!("{}", raw);
if !(200..300).contains(&status) || result["success"] != true {
eprintln!(
"API rejection. Inspect error.code/request_id; reconcile mutations before retrying."
);
return Ok(1);
}
Ok(0)
}
fn main() {
process::exit(match execute() {
Ok(code) => code,
Err(_) => {
eprintln!("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.");
2
}
})
}
```
```rust
[package]
name = "kdpika-api-example"
version = "1.0.0"
edition = "2021"
rust-version = "1.82"
[dependencies]
ureq = { version = "=2.12.1", default-features = false, features = ["tls"] }
url = "=2.5.2"
serde_json = "=1.0.145"
# Keep the tested Rust1.82 toolchain compatible with this pinned lock.
zeroize = "=1.8.1"
```
Run ruby request.rb validate.
### Ruby
```ruby
# Purpose: inspect one client-callable Sandbox operation, not grant feature access.
# Requires Ruby2.6+ standard library. Run: ruby request.rb public-key
# Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
# Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
STDOUT.set_encoding(Encoding::UTF_8)
OPERATIONS = JSON.parse(<<~'JSON')
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
JSON
def required(name)
value = ENV.fetch(name, '')
raise 'configuration' if value.empty? || value.match?(/[\r\n]/)
value
end
begin
raise 'operation' unless ARGV.length == 1 && OPERATIONS.key?(ARGV[0])
name = ARGV[0]; op = OPERATIONS.fetch(name)
base = ENV.fetch('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').sub(%r{/+$}, '')
uri = URI.parse(base)
local = uri.scheme == 'http' && ['127.0.0.1','localhost'].include?(uri.host) && ENV['KDP_ALLOW_LOOPBACK'] == '1'
raise 'URL' unless uri.host && (uri.scheme == 'https' || local) && !uri.userinfo && !uri.query && !uri.fragment && uri.path == '/api/v1'
target = URI.parse(base + op.fetch('path'))
request = op.fetch('method') == 'GET' ? Net::HTTP::Get.new(target) : Net::HTTP::Post.new(target)
request['Accept'] = 'application/json'; request['User-Agent'] = 'KDPikaDocsExample/1.0 Ruby'
if op['auth'] == 'publicApp'
request['X-App-ID'] = required('KDP_APP_ID'); request['X-Environment'] = 'SANDBOX'
elsif op['auth'] == 'activation'
request['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
end
request['X-Installation-ID'] = required('KDP_INSTALLATION_ID') if name == 'validate'
if op['method'] == 'POST'
body = ENV.fetch('KDP_BODY_JSON', '{}'); raise 'JSON object' unless JSON.parse(body).is_a?(Hash)
request['Content-Type'] = 'application/json'; request.body = body
end
http = Net::HTTP.new(target.host, target.port, nil)
http.use_ssl = target.scheme == 'https'; http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 5; http.read_timeout = 15
response = http.request(request) # Net::HTTP does not follow redirects.
result = JSON.parse(response.body)
puts response.body
unless response.code.to_i.between?(200,299) && result['success'] == true
warn 'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.'; exit 1
end
rescue StandardError
warn 'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.'; exit 2
end
```
Run java -cp request.jar:gson.jar RequestKt validate.
### Kotlin
```kotlin
// Purpose: inspect one client-callable Sandbox operation; not an entitlement verifier.
// Requires Kotlin2.4.21/JVM17+, Gson2.14.0. Run compiled RequestKt with public-key.
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, never application/CI logs.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import kotlin.system.exitProcess
val operationsJson = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""".trimIndent()
fun required(name: String): String = (System.getenv(name) ?: "").also {
require(it.isNotEmpty() && !it.contains('\r') && !it.contains('\n')) { "Missing configuration." }
}
fun main(args: Array) {
try {
val operations = JsonParser.parseString(operationsJson).asJsonObject
require(args.size == 1 && operations.has(args[0]))
val name = args[0]; val op = operations.getAsJsonObject(name)
val base = (System.getenv("KDP_BASE_URL") ?: "https://licensing.kdpika.com/api/v1").trimEnd('/')
val uri = URI.create(base)
val local = uri.scheme == "http" && uri.host in listOf("localhost", "127.0.0.1") && System.getenv("KDP_ALLOW_LOOPBACK") == "1"
require(uri.host != null && (uri.scheme == "https" || local) && uri.userInfo == null && uri.query == null && uri.fragment == null && uri.path == "/api/v1")
val request = HttpRequest.newBuilder(URI.create(base + op["path"].asString)).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Kotlin")
when (op["auth"].asString) {
"publicApp" -> request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX")
"activation" -> request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"))
}
if (name == "validate") request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"))
if (op["method"].asString == "POST") {
val body = System.getenv("KDP_BODY_JSON") ?: "{}"
require(JsonParser.parseString(body).isJsonObject)
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body))
} else request.GET()
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build()
val response = client.send(request.build(), HttpResponse.BodyHandlers.ofString())
val result = JsonParser.parseString(response.body()).asJsonObject
System.out.write(response.body().toByteArray(Charsets.UTF_8)); System.out.write(10)
if (response.statusCode() !in 200..299 || result["success"] != JsonPrimitive(true)) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); exitProcess(1)
}
} catch (error: Exception) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); exitProcess(2)
}
}
```
Run ./request validate.
### Swift
```swift
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Swift5.7+, Foundation; FoundationNetworking on Linux.
// Build: swiftc -parse-as-library request.swift -o request; run: ./request public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let contract = #"""
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
"""#
enum SetupError: Error {case invalid}
func required(_ name:String) throws -> String {
let value=ProcessInfo.processInfo.environment[name] ?? ""
guard !value.isEmpty && !value.contains("\r") && !value.contains("\n") else {throw SetupError.invalid};return value
}
final class NoRedirect: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(_ session:URLSession,task:URLSessionTask,willPerformHTTPRedirection response:HTTPURLResponse,newRequest request:URLRequest,completionHandler:@escaping(URLRequest?)->Void){completionHandler(nil)}
}
@main struct Example {
static func main() async {
do {
let env=ProcessInfo.processInfo.environment
let operations=try JSONSerialization.jsonObject(with:Data(contract.utf8)) as! [String:[String:String]]
guard CommandLine.arguments.count==2,let op=operations[CommandLine.arguments[1]] else {throw SetupError.invalid}
let name=CommandLine.arguments[1]
let base=(env["KDP_BASE_URL"] ?? "https://licensing.kdpika.com/api/v1").replacingOccurrences(of:"/+$",with:"",options:.regularExpression)
guard let parts=URLComponents(string:base),let host=parts.host else {throw SetupError.invalid}
let local=parts.scheme=="http" && ["localhost","127.0.0.1"].contains(host) && env["KDP_ALLOW_LOOPBACK"]=="1"
guard (parts.scheme=="https" || local) && parts.user==nil && parts.password==nil && parts.query==nil && parts.fragment==nil && parts.path=="/api/v1",let url=URL(string:base+op["path"]!) else {throw SetupError.invalid}
var request=URLRequest(url:url);request.httpMethod=op["method"];request.timeoutInterval=15
request.setValue("application/json",forHTTPHeaderField:"Accept");request.setValue("KDPikaDocsExample/1.0 Swift",forHTTPHeaderField:"User-Agent")
if op["auth"]=="publicApp" {request.setValue(try required("KDP_APP_ID"),forHTTPHeaderField:"X-App-ID");request.setValue("SANDBOX",forHTTPHeaderField:"X-Environment")}
if op["auth"]=="activation" {request.setValue("Bearer "+(try required("KDP_ACTIVATION_TOKEN")),forHTTPHeaderField:"Authorization")}
if name=="validate" {request.setValue(try required("KDP_INSTALLATION_ID"),forHTTPHeaderField:"X-Installation-ID")}
if op["method"]=="POST" {
let body=Data((env["KDP_BODY_JSON"] ?? "{}").utf8)
guard try JSONSerialization.jsonObject(with:body) is [String:Any] else {throw SetupError.invalid}
request.httpBody=body;request.setValue("application/json",forHTTPHeaderField:"Content-Type")
}
let session=URLSession(configuration:.ephemeral,delegate:NoRedirect(),delegateQueue:nil)
defer {session.invalidateAndCancel()}
let (raw,response)=try await session.data(for:request)
guard let http=response as? HTTPURLResponse,let result=try JSONSerialization.jsonObject(with:raw) as? [String:Any],let text=String(data:raw,encoding:.utf8) else {throw SetupError.invalid}
_ = text // Decoding above checks UTF-8 without reserializing JSON.
FileHandle.standardOutput.write(raw);FileHandle.standardOutput.write(Data([10]))
guard (200..<300).contains(http.statusCode),result["success"] as? Bool == true else {
FileHandle.standardError.write(Data("API rejection. Inspect error.code/request_id; reconcile mutations before retrying.\n".utf8));exit(1)
}
} catch {
FileHandle.standardError.write(Data("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n".utf8));exit(2)
}
}
}
```
Run dart run request.dart validate.
### Dart
```dart
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Dart3.7+ standard library. Run: dart run request.dart public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body. Private terminal only; output can contain secrets.
import 'dart:convert';
import 'dart:io';
const contract = r'''{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}''';
String required(String name) {
final value = Platform.environment[name] ?? '';
if (value.isEmpty || value.contains('\r') || value.contains('\n'))
throw StateError('configuration');
return value;
}
Future main(List args) async {
stdout.encoding = utf8;
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
try {
final operations = jsonDecode(contract) as Map;
if (args.length != 1 || !operations.containsKey(args[0]))
throw StateError('operation');
final name = args[0];
final op = operations[name] as Map;
final base =
(Platform.environment['KDP_BASE_URL'] ??
'https://licensing.kdpika.com/api/v1')
.replaceFirst(RegExp(r'/+$'), '');
final uri = Uri.parse(base);
final local =
uri.scheme == 'http' &&
['127.0.0.1', 'localhost'].contains(uri.host) &&
Platform.environment['KDP_ALLOW_LOOPBACK'] == '1';
if (uri.host.isEmpty ||
(uri.scheme != 'https' && !local) ||
uri.userInfo.isNotEmpty ||
uri.hasQuery ||
uri.hasFragment ||
uri.path != '/api/v1')
throw StateError('URL');
final request = await client
.openUrl(
op['method'] as String,
Uri.parse(base + (op['path'] as String)),
)
.timeout(const Duration(seconds: 15));
request.followRedirects = false;
request.headers.set('Accept', 'application/json');
request.headers.set('User-Agent', 'KDPikaDocsExample/1.0 Dart');
if (op['auth'] == 'publicApp') {
request.headers.set('X-App-ID', required('KDP_APP_ID'));
request.headers.set('X-Environment', 'SANDBOX');
}
if (op['auth'] == 'activation')
request.headers.set(
'Authorization',
'Bearer ${required('KDP_ACTIVATION_TOKEN')}',
);
if (name == 'validate')
request.headers.set('X-Installation-ID', required('KDP_INSTALLATION_ID'));
if (op['method'] == 'POST') {
final body = Platform.environment['KDP_BODY_JSON'] ?? '{}';
if (jsonDecode(body) is! Map) throw StateError('JSON object');
request.headers.set('Content-Type', 'application/json');
request.add(utf8.encode(body));
}
final response = await request.close().timeout(const Duration(seconds: 15));
final raw = await utf8.decoder
.bind(response)
.join()
.timeout(const Duration(seconds: 15));
final result = jsonDecode(raw) as Map;
stdout.writeln(raw);
if (response.statusCode < 200 ||
response.statusCode >= 300 ||
result['success'] != true) {
stderr.writeln(
'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.',
);
exitCode = 1;
}
} catch (error) {
stderr.writeln(
'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.',
);
exitCode = 2;
} finally {
client.close(force: true);
}
}
```
Run ./request-c validate.
### C
```c
/* C11 entry point. Build with client_http.c include, yyjson0.13.0 and libcurl7.76+.
* Run request public-key; expected JSON, exit0 success/1 rejection/2 unavailable. */
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```c
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
Run ./request-cpp validate.
### C++
```cpp
// C++17 entry point using the complete adjacent native transport.
// Requires yyjson0.13.0 and libcurl7.76+. Run request public-key; JSON/exit0,1,2.
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```cpp
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); 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](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
| INVALID_TOKEN (HTTP 401) | Missing/non-act_/unknown/revoked/deactivated activation token | Reactivate eligible installation |
| INVALID_REQUEST (HTTP 422) | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry |
| DEVICE_NOT_AUTHORIZED (HTTP 403) | Token is active but associated device is not ACTIVE | Reactivation if eligible |
| DEVICE_MISMATCH (HTTP 403) | Provided installation ID does not match token-bound HMAC hash | Use original installation; do not copy token |
| LICENSE_EXPIRED (HTTP 403) | Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTP | Renew eligible license |
| LICENSE_REVOKED (HTTP 403) | License REVOKED and no elapsed deadline taking precedence | Do not retry; contact developer |
| LICENSE_SUSPENDED (HTTP 403) | License SUSPENDED and no elapsed deadline taking precedence | Restore then reactivate |
| LICENSE_CANCELLED (HTTP 403) | License CANCELLED and no elapsed deadline taking precedence | Do not retry; new eligible license |
| LICENSE_PENDING (HTTP 403) | License PENDING and no elapsed deadline taking precedence | Wait for authorized activation |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Go live safely
Canonical: https://licensing.kdpika.com/docs/v1/integration-guide/go-live
---
slug: "/docs/v1/integration-guide/go-live"
title: "Go live safely"
description: "Stage 14: go live safely with a clear outcome, verification steps and one next action."
section: "Integration Guide"
order: 29
audience: "all"
level: "intermediate"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "guide"
---
## What you will do
Make a deliberate production decision, not just change a label.
1. Resolve the runtime, canonicalization and storage limits relevant to your payloads. The current SDK known defects can block a general production sign-off; a passing demonstration is not universal certification.
2. Use a platform entitlement that permits **[Live](/docs/v1/start/glossary#live)**. Free [Sandbox](/docs/v1/start/glossary#sandbox) is evaluation only.
3. Create or select separate Live [product](/docs/v1/start/glossary#product)/plan/[license](/docs/v1/start/glossary#license) records and correctly scoped backend credentials. Test keys do not become Live keys.
4. Keep the [public App ID](/docs/v1/start/glossary#public-app-id), environment and installation binding correct. Change the example's explicit SANDBOX setting only in your assessed production configuration.
5. Store backend secrets outside the client and web root; rotate exposed credentials.
6. Deploy HTTPS, verified signatures, required-feature gates, trusted payment verification and private state with tested permissions.
7. Add health/status monitoring, safe logs, support instructions and a rollback plan.
8. Run a final controlled Live smoke test only with your authorized payment/license workflow; do not treat a return URL as confirmation.
## Launch checklist
```text
[ ] Supported runtime and actual production payloads verified
[ ] Known SDK compatibility/storage blockers resolved
[ ] Live entitlement and separate Live records confirmed
[ ] No management secrets in customer apps
[ ] Signature, binding, time and feature gates fail closed
[ ] Payment, cancellation, refund and duplicate delivery tested
[ ] Private installation/token storage tested on supported OSs
[ ] Monitoring, support, credential rotation and rollback ready
```
Use Copy on this block, Download Markdown, or the roadmap's selected-language full-guide export.
## Expected result
You have an evidence-backed launch checklist. If a compatibility blocker remains, your integration stays in Sandbox until it is resolved.
## If it does not work
Ask support for help with the specific blocker. Do not claim an SDK is safe for all payloads or all operating systems on the basis of the narrow quickstart.
## Stage checklist
- Every launch item is checked or explicitly blocks release.
- My support and rollback path are ready.
## Next steps
[Review your go-live checklist](/docs/v1/start/go-live-checklist)
# Deactivate an installation
Canonical: https://licensing.kdpika.com/docs/v1/api/deactivate
---
title: "Deactivate an installation"
description: "Release this installation device seat."
slug: "/docs/v1/api/deactivate"
section: "API reference"
order: 30
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Release this installation [device](/docs/v1/start/glossary#device) seat.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/[licenses](/docs/v1/start/glossary#license)/deactivate
### When to use it
Release this installation's active device seat when the user signs out or transfers the license to another device. Only the installation [activation token](/docs/v1/start/glossary#activation-token) is required.
## Authentication and environment
Use Authorization: Bearer YOUR_ACTIVATION_TOKEN. The token fixes the installation/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Installation token | Bearer YOUR_ACTIVATION_TOKEN |
## 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
```json
{}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"deactivated": true,
"request_id": "req_29039d899dd3655d7ada1c89"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| deactivated | boolean | yes | const=true | Device seat released; cumulative count is unchanged. | true |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_29039d899dd3655d7ada1c89" |
### Response headers
X-Request-ID identifies the current request. No limiter headers are returned by this endpoint.
Errors omit those rate headers and Retry-After. See the complete [rate-limit reference](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| INVALID_TOKEN | 401 | Missing/non-act_/unknown/revoked/deactivated activation token | Reactivate eligible installation | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only 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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Marks the bound device DEACTIVATED and its active tokens DEACTIVATED, releasing the concurrent seat. Does not decrease activation_count. Audit/[webhook](/docs/v1/start/glossary#webhook) license.deactivated are emitted; no email is sent directly. Other installations remain active. It does not require an otherwise usable license, but the supplied token must still be active.
Authenticated requests can consume the [organization](/docs/v1/start/glossary#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. The first accepted call returns 200; the same token subsequently returns 401 INVALID_TOKEN, not a successful replay. A lost response therefore requires inspecting the application's saved state and, if necessary, [developer](/docs/v1/start/glossary#developer) support. Do not treat every unknown token as proof that this particular deactivation succeeded.
## Rate limits
No dedicated deactivation endpoint limiter is currently implemented. Do not invent an unlimited-service promise; use reasonable client throttling and avoid loops.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh deactivate.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py deactivate.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs deactivate.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php deactivate.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- deactivate.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
Run node compiled/request.js deactivate.
### TypeScript
```typescript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: TypeScript7.0.2, @types/node22.19.7, Node18.20+ native fetch, trusted server/Electron main process.
// Run compiled request.js with public-key after strict TypeScript compilation. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations: Readonly> = Object.freeze({
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
});
function required(name: string): string {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2] ?? '';
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers: Record = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run java -cp .:gson.jar Request deactivate.
### Java
```java
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Java17+, Gson2.14.0. Run: java -cp .:gson.jar Request public-key
// Windows classpaths use semicolons. Expected: JSON; exit0 success/1 API error/2 setup or transport.
// Credentials/body: KDP_* environment variables. Private terminal only; output may contain secrets.
import java.net.URI;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import com.google.gson.*;
public final class Request {
static final String CONTRACT = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""";
static String required(String name) {
String value = System.getenv(name);
if (value == null || value.isEmpty() || value.contains("\r") || value.contains("\n")) throw new IllegalArgumentException("Missing configuration.");
return value;
}
public static void main(String[] args) {
try {
JsonObject operations = JsonParser.parseString(CONTRACT).getAsJsonObject();
if (args.length != 1 || !operations.has(args[0])) throw new IllegalArgumentException("Unknown operation.");
String name = args[0]; JsonObject op = operations.getAsJsonObject(name);
String base = System.getenv().getOrDefault("KDP_BASE_URL", "https://licensing.kdpika.com/api/v1").replaceAll("/+$", "");
URI uri = URI.create(base);
boolean local = "http".equals(uri.getScheme()) && ("localhost".equals(uri.getHost()) || "127.0.0.1".equals(uri.getHost())) && "1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if (uri.getHost() == null || (!"https".equals(uri.getScheme()) && !local) || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null || !"/api/v1".equals(uri.getPath())) throw new IllegalArgumentException("Invalid base URL.");
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(base + op.get("path").getAsString())).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Java");
String auth = op.get("auth").getAsString();
if (auth.equals("publicApp")) request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX");
if (auth.equals("activation")) request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"));
if (name.equals("validate")) request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"));
if (op.get("method").getAsString().equals("POST")) {
String body = System.getenv().getOrDefault("KDP_BODY_JSON", "{}");
if (!JsonParser.parseString(body).isJsonObject()) throw new IllegalArgumentException("JSON object required.");
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8));
} else request.GET();
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
HttpResponse response = client.send(request.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
JsonObject result = JsonParser.parseString(response.body()).getAsJsonObject();
System.out.write(response.body().getBytes(StandardCharsets.UTF_8)); System.out.write(10);
if (response.statusCode() < 200 || response.statusCode() >= 300 || !result.has("success") || !result.get("success").equals(new JsonPrimitive(true))) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); System.exit(1);
}
} catch (Exception error) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); System.exit(2);
}
}
}
```
Run go run request.go deactivate.
### Go
```go
// Purpose: inspect one client-callable Sandbox operation, not grant access.
// Requires Go1.20+ standard library. Run: go run request.go public-key
// Expected: JSON, exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables; output may contain secrets. Private terminal only.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
)
type operation struct {
Method string `json:"method"`
Path string `json:"path"`
Auth string `json:"auth"`
}
const contract = `{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}`
func required(name string) string {
v := os.Getenv(name)
if v == "" || strings.ContainsAny(v, "\r\n") {
panic("configuration")
}
return v
}
func execute() int {
var operations map[string]operation
if json.Unmarshal([]byte(contract), &operations) != nil || len(os.Args) != 2 {
panic("operation")
}
name := os.Args[1]
op, ok := operations[name]
if !ok {
panic("operation")
}
base := os.Getenv("KDP_BASE_URL")
if base == "" {
base = "https://licensing.kdpika.com/api/v1"
}
base = strings.TrimRight(base, "/")
u, err := url.Parse(base)
if err != nil {
panic("URL")
}
local := u.Scheme == "http" && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") && os.Getenv("KDP_ALLOW_LOOPBACK") == "1"
if u.Hostname() == "" || (u.Scheme != "https" && !local) || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/api/v1" {
panic("URL")
}
body := ""
if op.Method == "POST" {
body = os.Getenv("KDP_BODY_JSON")
if body == "" {
body = "{}"
}
var object map[string]json.RawMessage
if json.Unmarshal([]byte(body), &object) != nil || object == nil {
panic("body")
}
}
request, err := http.NewRequest(op.Method, base+op.Path, strings.NewReader(body))
if err != nil {
panic("request")
}
request.Header.Set("Accept", "application/json")
request.Header.Set("User-Agent", "KDPikaDocsExample/1.0 Go")
if op.Method == "POST" {
request.Header.Set("Content-Type", "application/json")
}
if op.Auth == "publicApp" {
request.Header.Set("X-App-ID", required("KDP_APP_ID"))
request.Header.Set("X-Environment", "SANDBOX")
}
if op.Auth == "activation" {
request.Header.Set("Authorization", "Bearer "+required("KDP_ACTIVATION_TOKEN"))
}
if name == "validate" {
request.Header.Set("X-Installation-ID", required("KDP_INSTALLATION_ID"))
}
client := &http.Client{Timeout: 15 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
response, err := client.Do(request)
if err != nil {
panic("transport")
}
defer response.Body.Close()
raw, err := io.ReadAll(io.LimitReader(response.Body, 1048577))
if err != nil || len(raw) > 1048576 {
panic("response")
}
var result struct {
Success bool `json:"success"`
}
if json.Unmarshal(raw, &result) != nil {
panic("JSON")
}
fmt.Println(string(raw))
if response.StatusCode < 200 || response.StatusCode >= 300 || !result.Success {
fmt.Fprintln(os.Stderr, "API rejection. Inspect error.code/request_id; reconcile mutations before retrying.")
return 1
}
return 0
}
func main() {
code := 2
func() {
defer func() {
if recover() != nil {
fmt.Fprintln(os.Stderr, "Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.")
}
}()
code = execute()
}()
os.Exit(code)
}
```
Run cargo run --locked --manifest-path rust/Cargo.toml -- deactivate.
### Rust
```rust
// Purpose: inspect one client-callable Sandbox operation; not entitlement verification.
// Requires Rust1.82+, pinned Cargo.toml/Cargo.lock. Run: cargo run -- public-key
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, output may contain secrets.
use serde_json::Value;
use std::{env, process, time::Duration};
const CONTRACT: &str = r#"{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}"#;
fn required(name: &str) -> Result> {
let value = env::var(name)?;
if value.is_empty() || value.contains('\r') || value.contains('\n') {
return Err("configuration".into());
};
Ok(value)
}
fn execute() -> Result> {
let operations: Value = serde_json::from_str(CONTRACT)?;
let args: Vec = env::args().collect();
if args.len() != 2 {
return Err("operation".into());
}
let name = &args[1];
let op = operations.get(name).ok_or("operation")?;
let base = env::var("KDP_BASE_URL")
.unwrap_or_else(|_| "https://licensing.kdpika.com/api/v1".into())
.trim_end_matches('/')
.to_string();
let uri = url::Url::parse(&base)?;
let local = uri.scheme() == "http"
&& matches!(uri.host_str(), Some("127.0.0.1" | "localhost"))
&& env::var("KDP_ALLOW_LOOPBACK").unwrap_or_default() == "1";
if uri.host_str().is_none()
|| (uri.scheme() != "https" && !local)
|| !uri.username().is_empty()
|| uri.password().is_some()
|| uri.query().is_some()
|| uri.fragment().is_some()
|| uri.path() != "/api/v1"
{
return Err("URL".into());
}
let method = op["method"].as_str().ok_or("method")?;
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(15))
.redirects(0)
.build();
let target = base + op["path"].as_str().ok_or("path")?;
let mut request = agent
.request(method, &target)
.set("Accept", "application/json")
.set("User-Agent", "KDPikaDocsExample/1.0 Rust");
if op["auth"] == "publicApp" {
request = request
.set("X-App-ID", &required("KDP_APP_ID")?)
.set("X-Environment", "SANDBOX")
}
if op["auth"] == "activation" {
request = request.set(
"Authorization",
&("Bearer ".to_string() + &required("KDP_ACTIVATION_TOKEN")?),
)
}
if name == "validate" {
request = request.set("X-Installation-ID", &required("KDP_INSTALLATION_ID")?)
}
let answer = if method == "POST" {
let body = env::var("KDP_BODY_JSON").unwrap_or_else(|_| "{}".into());
if !serde_json::from_str::(&body)?.is_object() {
return Err("body".into());
}
request
.set("Content-Type", "application/json")
.send_string(&body)
} else {
request.call()
};
let response = match answer {
Ok(response) => response,
Err(ureq::Error::Status(_, response)) => response,
Err(error) => return Err(Box::new(error)),
};
let status = response.status();
let raw = response.into_string()?;
let result: Value = serde_json::from_str(&raw)?;
println!("{}", raw);
if !(200..300).contains(&status) || result["success"] != true {
eprintln!(
"API rejection. Inspect error.code/request_id; reconcile mutations before retrying."
);
return Ok(1);
}
Ok(0)
}
fn main() {
process::exit(match execute() {
Ok(code) => code,
Err(_) => {
eprintln!("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.");
2
}
})
}
```
```rust
[package]
name = "kdpika-api-example"
version = "1.0.0"
edition = "2021"
rust-version = "1.82"
[dependencies]
ureq = { version = "=2.12.1", default-features = false, features = ["tls"] }
url = "=2.5.2"
serde_json = "=1.0.145"
# Keep the tested Rust1.82 toolchain compatible with this pinned lock.
zeroize = "=1.8.1"
```
Run ruby request.rb deactivate.
### Ruby
```ruby
# Purpose: inspect one client-callable Sandbox operation, not grant feature access.
# Requires Ruby2.6+ standard library. Run: ruby request.rb public-key
# Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
# Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
require 'json'
require 'net/http'
require 'uri'
require 'openssl'
STDOUT.set_encoding(Encoding::UTF_8)
OPERATIONS = JSON.parse(<<~'JSON')
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
JSON
def required(name)
value = ENV.fetch(name, '')
raise 'configuration' if value.empty? || value.match?(/[\r\n]/)
value
end
begin
raise 'operation' unless ARGV.length == 1 && OPERATIONS.key?(ARGV[0])
name = ARGV[0]; op = OPERATIONS.fetch(name)
base = ENV.fetch('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').sub(%r{/+$}, '')
uri = URI.parse(base)
local = uri.scheme == 'http' && ['127.0.0.1','localhost'].include?(uri.host) && ENV['KDP_ALLOW_LOOPBACK'] == '1'
raise 'URL' unless uri.host && (uri.scheme == 'https' || local) && !uri.userinfo && !uri.query && !uri.fragment && uri.path == '/api/v1'
target = URI.parse(base + op.fetch('path'))
request = op.fetch('method') == 'GET' ? Net::HTTP::Get.new(target) : Net::HTTP::Post.new(target)
request['Accept'] = 'application/json'; request['User-Agent'] = 'KDPikaDocsExample/1.0 Ruby'
if op['auth'] == 'publicApp'
request['X-App-ID'] = required('KDP_APP_ID'); request['X-Environment'] = 'SANDBOX'
elsif op['auth'] == 'activation'
request['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
end
request['X-Installation-ID'] = required('KDP_INSTALLATION_ID') if name == 'validate'
if op['method'] == 'POST'
body = ENV.fetch('KDP_BODY_JSON', '{}'); raise 'JSON object' unless JSON.parse(body).is_a?(Hash)
request['Content-Type'] = 'application/json'; request.body = body
end
http = Net::HTTP.new(target.host, target.port, nil)
http.use_ssl = target.scheme == 'https'; http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.open_timeout = 5; http.read_timeout = 15
response = http.request(request) # Net::HTTP does not follow redirects.
result = JSON.parse(response.body)
puts response.body
unless response.code.to_i.between?(200,299) && result['success'] == true
warn 'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.'; exit 1
end
rescue StandardError
warn 'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.'; exit 2
end
```
Run java -cp request.jar:gson.jar RequestKt deactivate.
### Kotlin
```kotlin
// Purpose: inspect one client-callable Sandbox operation; not an entitlement verifier.
// Requires Kotlin2.4.21/JVM17+, Gson2.14.0. Run compiled RequestKt with public-key.
// Expected: raw JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body; private terminal only, never application/CI logs.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.time.Duration
import com.google.gson.JsonParser
import com.google.gson.JsonPrimitive
import kotlin.system.exitProcess
val operationsJson = """
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
""".trimIndent()
fun required(name: String): String = (System.getenv(name) ?: "").also {
require(it.isNotEmpty() && !it.contains('\r') && !it.contains('\n')) { "Missing configuration." }
}
fun main(args: Array) {
try {
val operations = JsonParser.parseString(operationsJson).asJsonObject
require(args.size == 1 && operations.has(args[0]))
val name = args[0]; val op = operations.getAsJsonObject(name)
val base = (System.getenv("KDP_BASE_URL") ?: "https://licensing.kdpika.com/api/v1").trimEnd('/')
val uri = URI.create(base)
val local = uri.scheme == "http" && uri.host in listOf("localhost", "127.0.0.1") && System.getenv("KDP_ALLOW_LOOPBACK") == "1"
require(uri.host != null && (uri.scheme == "https" || local) && uri.userInfo == null && uri.query == null && uri.fragment == null && uri.path == "/api/v1")
val request = HttpRequest.newBuilder(URI.create(base + op["path"].asString)).timeout(Duration.ofSeconds(15))
.header("Accept", "application/json").header("User-Agent", "KDPikaDocsExample/1.0 Kotlin")
when (op["auth"].asString) {
"publicApp" -> request.header("X-App-ID", required("KDP_APP_ID")).header("X-Environment", "SANDBOX")
"activation" -> request.header("Authorization", "Bearer " + required("KDP_ACTIVATION_TOKEN"))
}
if (name == "validate") request.header("X-Installation-ID", required("KDP_INSTALLATION_ID"))
if (op["method"].asString == "POST") {
val body = System.getenv("KDP_BODY_JSON") ?: "{}"
require(JsonParser.parseString(body).isJsonObject)
request.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body))
} else request.GET()
val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build()
val response = client.send(request.build(), HttpResponse.BodyHandlers.ofString())
val result = JsonParser.parseString(response.body()).asJsonObject
System.out.write(response.body().toByteArray(Charsets.UTF_8)); System.out.write(10)
if (response.statusCode() !in 200..299 || result["success"] != JsonPrimitive(true)) {
System.err.println("API rejection. Inspect error.code/request_id; reconcile mutations before retrying."); exitProcess(1)
}
} catch (error: Exception) {
System.err.println("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); exitProcess(2)
}
}
```
Run ./request deactivate.
### Swift
```swift
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Swift5.7+, Foundation; FoundationNetworking on Linux.
// Build: swiftc -parse-as-library request.swift -o request; run: ./request public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// Credentials/body: KDP_* variables. Private terminal only; output may contain secrets.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
let contract = #"""
{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}
"""#
enum SetupError: Error {case invalid}
func required(_ name:String) throws -> String {
let value=ProcessInfo.processInfo.environment[name] ?? ""
guard !value.isEmpty && !value.contains("\r") && !value.contains("\n") else {throw SetupError.invalid};return value
}
final class NoRedirect: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(_ session:URLSession,task:URLSessionTask,willPerformHTTPRedirection response:HTTPURLResponse,newRequest request:URLRequest,completionHandler:@escaping(URLRequest?)->Void){completionHandler(nil)}
}
@main struct Example {
static func main() async {
do {
let env=ProcessInfo.processInfo.environment
let operations=try JSONSerialization.jsonObject(with:Data(contract.utf8)) as! [String:[String:String]]
guard CommandLine.arguments.count==2,let op=operations[CommandLine.arguments[1]] else {throw SetupError.invalid}
let name=CommandLine.arguments[1]
let base=(env["KDP_BASE_URL"] ?? "https://licensing.kdpika.com/api/v1").replacingOccurrences(of:"/+$",with:"",options:.regularExpression)
guard let parts=URLComponents(string:base),let host=parts.host else {throw SetupError.invalid}
let local=parts.scheme=="http" && ["localhost","127.0.0.1"].contains(host) && env["KDP_ALLOW_LOOPBACK"]=="1"
guard (parts.scheme=="https" || local) && parts.user==nil && parts.password==nil && parts.query==nil && parts.fragment==nil && parts.path=="/api/v1",let url=URL(string:base+op["path"]!) else {throw SetupError.invalid}
var request=URLRequest(url:url);request.httpMethod=op["method"];request.timeoutInterval=15
request.setValue("application/json",forHTTPHeaderField:"Accept");request.setValue("KDPikaDocsExample/1.0 Swift",forHTTPHeaderField:"User-Agent")
if op["auth"]=="publicApp" {request.setValue(try required("KDP_APP_ID"),forHTTPHeaderField:"X-App-ID");request.setValue("SANDBOX",forHTTPHeaderField:"X-Environment")}
if op["auth"]=="activation" {request.setValue("Bearer "+(try required("KDP_ACTIVATION_TOKEN")),forHTTPHeaderField:"Authorization")}
if name=="validate" {request.setValue(try required("KDP_INSTALLATION_ID"),forHTTPHeaderField:"X-Installation-ID")}
if op["method"]=="POST" {
let body=Data((env["KDP_BODY_JSON"] ?? "{}").utf8)
guard try JSONSerialization.jsonObject(with:body) is [String:Any] else {throw SetupError.invalid}
request.httpBody=body;request.setValue("application/json",forHTTPHeaderField:"Content-Type")
}
let session=URLSession(configuration:.ephemeral,delegate:NoRedirect(),delegateQueue:nil)
defer {session.invalidateAndCancel()}
let (raw,response)=try await session.data(for:request)
guard let http=response as? HTTPURLResponse,let result=try JSONSerialization.jsonObject(with:raw) as? [String:Any],let text=String(data:raw,encoding:.utf8) else {throw SetupError.invalid}
_ = text // Decoding above checks UTF-8 without reserializing JSON.
FileHandle.standardOutput.write(raw);FileHandle.standardOutput.write(Data([10]))
guard (200..<300).contains(http.statusCode),result["success"] as? Bool == true else {
FileHandle.standardError.write(Data("API rejection. Inspect error.code/request_id; reconcile mutations before retrying.\n".utf8));exit(1)
}
} catch {
FileHandle.standardError.write(Data("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n".utf8));exit(2)
}
}
}
```
Run dart run request.dart deactivate.
### Dart
```dart
// Purpose: inspect one client-callable Sandbox operation, not grant feature access.
// Requires Dart3.7+ standard library. Run: dart run request.dart public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* variables carry credentials/body. Private terminal only; output can contain secrets.
import 'dart:convert';
import 'dart:io';
const contract = r'''{
"public-key": {
"method": "GET",
"path": "/public-key",
"auth": "public"
},
"activate": {
"method": "POST",
"path": "/licenses/activate",
"auth": "publicApp"
},
"validate": {
"method": "POST",
"path": "/licenses/validate",
"auth": "activation"
},
"deactivate": {
"method": "POST",
"path": "/licenses/deactivate",
"auth": "activation"
}
}''';
String required(String name) {
final value = Platform.environment[name] ?? '';
if (value.isEmpty || value.contains('\r') || value.contains('\n'))
throw StateError('configuration');
return value;
}
Future main(List args) async {
stdout.encoding = utf8;
final client = HttpClient()..connectionTimeout = const Duration(seconds: 5);
try {
final operations = jsonDecode(contract) as Map;
if (args.length != 1 || !operations.containsKey(args[0]))
throw StateError('operation');
final name = args[0];
final op = operations[name] as Map;
final base =
(Platform.environment['KDP_BASE_URL'] ??
'https://licensing.kdpika.com/api/v1')
.replaceFirst(RegExp(r'/+$'), '');
final uri = Uri.parse(base);
final local =
uri.scheme == 'http' &&
['127.0.0.1', 'localhost'].contains(uri.host) &&
Platform.environment['KDP_ALLOW_LOOPBACK'] == '1';
if (uri.host.isEmpty ||
(uri.scheme != 'https' && !local) ||
uri.userInfo.isNotEmpty ||
uri.hasQuery ||
uri.hasFragment ||
uri.path != '/api/v1')
throw StateError('URL');
final request = await client
.openUrl(
op['method'] as String,
Uri.parse(base + (op['path'] as String)),
)
.timeout(const Duration(seconds: 15));
request.followRedirects = false;
request.headers.set('Accept', 'application/json');
request.headers.set('User-Agent', 'KDPikaDocsExample/1.0 Dart');
if (op['auth'] == 'publicApp') {
request.headers.set('X-App-ID', required('KDP_APP_ID'));
request.headers.set('X-Environment', 'SANDBOX');
}
if (op['auth'] == 'activation')
request.headers.set(
'Authorization',
'Bearer ${required('KDP_ACTIVATION_TOKEN')}',
);
if (name == 'validate')
request.headers.set('X-Installation-ID', required('KDP_INSTALLATION_ID'));
if (op['method'] == 'POST') {
final body = Platform.environment['KDP_BODY_JSON'] ?? '{}';
if (jsonDecode(body) is! Map) throw StateError('JSON object');
request.headers.set('Content-Type', 'application/json');
request.add(utf8.encode(body));
}
final response = await request.close().timeout(const Duration(seconds: 15));
final raw = await utf8.decoder
.bind(response)
.join()
.timeout(const Duration(seconds: 15));
final result = jsonDecode(raw) as Map;
stdout.writeln(raw);
if (response.statusCode < 200 ||
response.statusCode >= 300 ||
result['success'] != true) {
stderr.writeln(
'API rejection. Inspect error.code/request_id; reconcile mutations before retrying.',
);
exitCode = 1;
}
} catch (error) {
stderr.writeln(
'Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.',
);
exitCode = 2;
} finally {
client.close(force: true);
}
}
```
Run ./request-c deactivate.
### C
```c
/* C11 entry point. Build with client_http.c include, yyjson0.13.0 and libcurl7.76+.
* Run request public-key; expected JSON, exit0 success/1 rejection/2 unavailable. */
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```c
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
Run ./request-cpp deactivate.
### C++
```cpp
// C++17 entry point using the complete adjacent native transport.
// Requires yyjson0.13.0 and libcurl7.76+. Run request public-key; JSON/exit0,1,2.
#include "client_http.c"
int main(int argc,char **argv){return kdpika_example_main(argc,argv);}
```
```cpp
/* Purpose: shared native libcurl transport for C11 and C++17 Sandbox examples.
* Requires libcurl7.76+, yyjson0.13.0. Build commands are in the sample README.
* Run request public-key. JSON stdout; exit0 success/1 rejection/2 setup/transport.
* Credentials/body: KDP_* environment. Private terminal only; output may contain secrets.
* Transport inspection only: verify signed state and policy before authorizing access.
*/
#include
#include
#include
#include
#include
#include "yyjson.h"
static const char *kdp_contract="{\"public-key\":{\"method\":\"GET\",\"path\":\"/public-key\",\"auth\":\"public\"},\"activate\":{\"method\":\"POST\",\"path\":\"/licenses/activate\",\"auth\":\"publicApp\"},\"validate\":{\"method\":\"POST\",\"path\":\"/licenses/validate\",\"auth\":\"activation\"},\"deactivate\":{\"method\":\"POST\",\"path\":\"/licenses/deactivate\",\"auth\":\"activation\"}}";
typedef struct {char *data;size_t size;} kdp_buffer;
static size_t kdp_receive(char *data,size_t size,size_t count,void *context){
kdp_buffer *b=(kdp_buffer *)context;
if(size && count>SIZE_MAX/size)return 0;
size_t n=size*count;if(n>1048576-b->size)return 0;
char *p=(char *)realloc(b->data,b->size+n+1);if(!p)return 0;
b->data=p;memcpy(p+b->size,data,n);b->size+=n;p[b->size]=0;return n;
}
static const char *kdp_env(const char *name){const char *v=getenv(name);return v?v:"";}
static const char *kdp_required(const char *name){const char *v=kdp_env(name);return *v&&!strchr(v,'\r')&&!strchr(v,'\n')?v:NULL;}
static int kdp_header(struct curl_slist **headers,const char *name,const char *value){
if(!value)return 0;size_t n=strlen(name)+strlen(value)+3;char *line=(char *)malloc(n);if(!line)return 0;
snprintf(line,n,"%s: %s",name,value);struct curl_slist *next=curl_slist_append(*headers,line);free(line);
if(!next)return 0;*headers=next;return 1;
}
static int kdpika_example_main(int argc,char **argv){
int code=2;CURL *curl=NULL;CURLU *url=NULL;struct curl_slist *headers=NULL;
yyjson_doc *ops=NULL,*bodydoc=NULL,*result=NULL;char *scheme=NULL,*host=NULL,*path=NULL,*part=NULL,*target=NULL;
kdp_buffer buffer={NULL,0};const char *base=kdp_env("KDP_BASE_URL"),*body=kdp_env("KDP_BODY_JSON");
const char *method=NULL,*route=NULL,*auth=NULL;yyjson_val *op=NULL;long status=0;
if(argc!=2)goto cleanup;
if(!*base)base="https://licensing.kdpika.com/api/v1";
if(!*body)body="{}";
ops=yyjson_read(kdp_contract,strlen(kdp_contract),0);if(!ops)goto cleanup;
op=yyjson_obj_get(yyjson_doc_get_root(ops),argv[1]);if(!op)goto cleanup;
method=yyjson_get_str(yyjson_obj_get(op,"method"));route=yyjson_get_str(yyjson_obj_get(op,"path"));auth=yyjson_get_str(yyjson_obj_get(op,"auth"));
if(!method||!route||!auth||curl_global_init(CURL_GLOBAL_DEFAULT)!=0)goto cleanup;
url=curl_url();if(!url||curl_url_set(url,CURLUPART_URL,base,0)!=CURLUE_OK)goto cleanup;
if(curl_url_get(url,CURLUPART_SCHEME,&scheme,0)||curl_url_get(url,CURLUPART_HOST,&host,0)||curl_url_get(url,CURLUPART_PATH,&path,0))goto cleanup;
if(strcmp(path,"/api/v1")!=0)goto cleanup;
if(strcmp(scheme,"https")!=0 && !(strcmp(scheme,"http")==0 && (!strcmp(host,"localhost")||!strcmp(host,"127.0.0.1")) && !strcmp(kdp_env("KDP_ALLOW_LOOPBACK"),"1")))goto cleanup;
{CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};
for(size_t i=0;i=200&&status<300&&yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(result),"success"))?0:1;
cleanup:
if(code)fprintf(stderr,"Request rejected, unavailable or configuration invalid. Inspect JSON error.code/request_id when returned; do not blindly retry mutations.\n");
if(curl)curl_easy_cleanup(curl);if(url)curl_url_cleanup(url);curl_slist_free_all(headers);
curl_free(scheme);curl_free(host);curl_free(path);curl_free(part);free(target);free(buffer.data);
yyjson_doc_free(ops);yyjson_doc_free(bodydoc);yyjson_doc_free(result);return code;
}
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| INVALID_TOKEN (HTTP 401) | Missing/non-act_/unknown/revoked/deactivated activation token | Reactivate eligible installation |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Retrieve a license
Canonical: https://licensing.kdpika.com/docs/v1/api/get-license
---
title: "Retrieve a license"
description: "Read a scoped unsigned license summary."
slug: "/docs/v1/api/get-license"
section: "API reference"
order: 31
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Read a scoped unsigned [license](/docs/v1/start/glossary#license) summary.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
GET /api/v1/licenses/{license}
### When to use it
Read an unsigned management summary from your trusted backend for support or reconciliation. This is not an [activation](/docs/v1/start/glossary#activation) response, a list operation or a replacement for signed installation validation.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:read. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.
## Request body
No request body is consumed. Send this GET request without a body.
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](/docs/v1/api/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.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"license_key_mask": "TESTDOCS-••••-••••-HAHH",
"status": "active",
"environment": "sandbox",
"starts_at": "2026-10-17T10:39:35+00:00",
"expires_at": "2026-11-16T10:39:35+00:00",
"created_at": "2026-10-17T12:39:35+00:00",
"updated_at": "2026-10-17T12:39:35+00:00",
"max_devices": 2,
"active_devices": 0,
"max_activations": 4,
"activation_count": 0,
"min_version": null,
"max_version": null,
"revocation_reason": null,
"product": {
"id": "app_a642cd24641ec976626b",
"name": "Docs fixture product"
},
"plan": {
"id": "plan_3bf54c099326639ee183661f",
"name": "Fixture plan",
"slug": "fixture-plan",
"billing_type": "monthly",
"price": 19.5,
"currency": "EUR"
},
"customer": {
"id": "cus_d7db5516f8377f89a869e293",
"name": "Synthetic contract customer",
"email": "contract-6b9f0843f1d3609f@example.invalid",
"company": null,
"country": null
}
},
"request_id": "req_2e71dc11a311d6a5d34c50cb"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | Unsigned management summary; no full [license key](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#activation-token). | Nested object/array shown in full JSON |
| license.id | string | yes | See description | License identifier. | "lic_578581bd80c2a672da30fc29" |
| license.license_key_mask | string | yes | See description | Display-only masked key. | "TESTDOCS-••••-••••-HAHH" |
| license.status | string | yes | enum=["active", "suspended", "revoked", "expired", "pending", "cancelled"] | Lowercase stored license status. | "active" |
| license.environment | string | yes | enum=["sandbox", "[live](/docs/v1/start/glossary#live)"] | Bound environment. | "sandbox" |
| license.starts_at | string or null | yes | format="date-time" | Configured start. | "2026-10-17T10:39:35+00:00" |
| license.expires_at | string or null | yes | format="date-time" | Configured expiry. | "2026-11-16T10:39:35+00:00" |
| license.created_at | string or null | yes | format="date-time" | Creation timestamp. | "2026-10-17T12:39:35+00:00" |
| license.updated_at | string or null | yes | format="date-time" | Last stored update. | "2026-10-17T12:39:35+00:00" |
| license.max_devices | integer | yes | See description | Concurrent [device](/docs/v1/start/glossary#device) limit. | 2 |
| license.active_devices | integer | yes | See description | Active device count. | 0 |
| license.max_activations | integer or null | yes | See description | Cumulative activation cap. | 4 |
| license.activation_count | integer | yes | See description | Consumed activations. | 0 |
| license.min_version | string or null | yes | See description | Lower version bound. | null |
| license.max_version | string or null | yes | See description | Upper version bound. | null |
| license.revocation_reason | string or null | yes | See description | Optional reason. | null |
| license.product | object | yes | See description | Product summary. | Nested object/array shown in full JSON |
| license.product.id | string | yes | See description | [Public App ID](/docs/v1/start/glossary#public-app-id). | "app_a642cd24641ec976626b" |
| license.product.name | string | yes | See description | Product name. | "Docs fixture product" |
| license.plan | object | yes | See description | Plan summary. | Nested object/array shown in full JSON |
| license.plan.id | string | yes | See description | Internal product-plan identifier. | "plan_3bf54c099326639ee183661f" |
| license.plan.name | string | yes | See description | Plan display name. | "Fixture plan" |
| license.plan.slug | string | yes | See description | Plan slug. | "fixture-plan" |
| license.plan.billing_type | string | yes | See description | Lowercase product-plan billing type. | "monthly" |
| license.plan.price | number | yes | See description | PHP float; preserve numeric tokens such as19.0 for signatures. | 19.5 |
| license.plan.currency | string | yes | See description | Configured product-plan currency. | "EUR" |
| license.customer | object | yes | See description | Customer information; personal data, not an authentication credential. | Nested object/array shown in full JSON |
| license.customer.id | string | yes | See description | Customer public identifier. | "cus_d7db5516f8377f89a869e293" |
| license.customer.name | string or null | yes | See description | Optional customer name. | "Synthetic contract customer" |
| license.customer.email | string | yes | See description | Customer email associated with this license. | "contract-6b9f0843f1d3609f@example.invalid" |
| license.customer.company | string or null | yes | See description | Optional company. | null |
| license.customer.country | string or null | yes | See description | Optional country. | null |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_2e71dc11a311d6a5d34c50cb" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Reads the scoped license/customer/product/plan summary and active-device count. Does not reveal the complete key, issue a token, mutate license state or emit an audit/[webhook](/docs/v1/start/glossary#webhook)/email. Successful key authentication updates the developer key's last-used timestamp. Customer fields are personal data: restrict the response to authorized operators.
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
GET is safe to retry with bounded backoff. No Idempotency-Key is consumed. A summary is a snapshot, not a caller version/ETag concurrency precondition; do not use it as a lock before another mutation.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Scoped reads:180 per60 seconds per developer-key license-read bucket.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh get-license.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py get-license.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs get-license.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php get-license.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- get-license.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Revoke a license
Canonical: https://licensing.kdpika.com/docs/v1/api/revoke
---
title: "Revoke a license"
description: "Revoke a license and its active tokens."
slug: "/docs/v1/api/revoke"
section: "API reference"
order: 32
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Revoke a [license](/docs/v1/start/glossary#license) and its active tokens.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses/{license}/revoke
### When to use it
Permanently revoke an eligible license after your trusted business workflow decides access should end. This API does not decide whether a payment refund/dispute warrants revocation.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.
## Request body
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| reason | string | no | maxLength=190 | Optional trimmed reason; invalid length becomes null. | "Customer support request" |
### Full request JSON
```json
{
"reason": "Customer support request"
}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"status": "revoked"
},
"request_id": "req_3a543bd6e559e7752e917105"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | State result; old revoked tokens do not revive. | Nested object/array shown in full JSON |
| license.id | string | yes | See description | License identifier. | "lic_578581bd80c2a672da30fc29" |
| license.status | string | yes | enum=["active", "suspended", "revoked"] | Resulting state. | "revoked" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_3a543bd6e559e7752e917105" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INVALID_STATE | 409 | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
ACTIVE, SUSPENDED, PENDING, EXPIRED or REVOKED may become REVOKED; CANCELLED cannot. Stores the optional revocation reason and revokes all active [activation tokens](/docs/v1/start/glossary#activation-token). [Device](/docs/v1/start/glossary#device) rows remain ACTIVE, so this is not a seat reset. Audit/[webhook](/docs/v1/start/glossary#webhook) license.revoked are emitted, including repeated accepted revocation; no email is sent directly.
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. Repeating an allowed revocation can repeat audit/webhook events. State updates use a database transaction but no caller revision precondition. A concurrent removal after the preliminary read can produce INVALID_STATE rather than the initial scoped LICENSE_NOT_FOUND.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Shared license-write bucket:60 per60 seconds per key across revoke, suspend, restore and renew.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh revoke.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py revoke.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs revoke.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php revoke.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- revoke.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INVALID_STATE (HTTP 409) | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Suspend a license
Canonical: https://licensing.kdpika.com/docs/v1/api/suspend
---
title: "Suspend a license"
description: "Suspend an eligible license and invalidate its active tokens."
slug: "/docs/v1/api/suspend"
section: "API reference"
order: 33
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Suspend an eligible [license](/docs/v1/start/glossary#license) and invalidate its active tokens.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses/{license}/suspend
### When to use it
Temporarily prevent an eligible license from being used, for example after your own store confirms a payment failure requiring suspension.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.
## Request body
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| reason | string | no | maxLength=190 | Optional trimmed reason; invalid length becomes null. | "Customer support request" |
### Full request JSON
```json
{
"reason": "Customer support request"
}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"status": "suspended"
},
"request_id": "req_793bbd0d0f1194351154241a"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | State result; old revoked tokens do not revive. | Nested object/array shown in full JSON |
| license.id | string | yes | See description | License identifier. | "lic_578581bd80c2a672da30fc29" |
| license.status | string | yes | enum=["active", "suspended", "revoked"] | Resulting state. | "suspended" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_793bbd0d0f1194351154241a" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INVALID_STATE | 409 | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
ACTIVE or SUSPENDED may become SUSPENDED. Revokes active tokens without releasing ACTIVE [device](/docs/v1/start/glossary#device) seats. The optional reason is audit/[webhook](/docs/v1/start/glossary#webhook) context; revocation_reason is cleared because the new status is not REVOKED. Audit/webhook license.suspended are emitted; no email is sent directly. A later restore does not revive old tokens.
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. An accepted repeat may emit again. Coordinate competing support/store state changes on your backend; there is no compare-and-swap revision parameter. Tokens revoked by this action require fresh [activation](/docs/v1/start/glossary#activation) after eligibility is restored.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Shared license-write bucket:60 per60 seconds per key across revoke, suspend, restore and renew.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh suspend.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py suspend.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs suspend.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php suspend.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- suspend.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INVALID_STATE (HTTP 409) | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Restore a license
Canonical: https://licensing.kdpika.com/docs/v1/api/restore
---
title: "Restore a license"
description: "Restore an eligible license without reviving old tokens."
slug: "/docs/v1/api/restore"
section: "API reference"
order: 34
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Restore an eligible [license](/docs/v1/start/glossary#license) without reviving old tokens.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses/{license}/restore
### When to use it
Return an eligible SUSPENDED or PENDING license to ACTIVE after the underlying issue is resolved. An already ACTIVE license can also accept this operation. Revoked/cancelled licenses cannot be restored.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.
## Request body
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| reason | string | no | maxLength=190 | Optional trimmed reason; invalid length becomes null. | "Customer support request" |
### Full request JSON
```json
{
"reason": "Customer support request"
}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"status": "active"
},
"request_id": "req_0dd8d2e77cf580ba8b5d491f"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | State result; old revoked tokens do not revive. | Nested object/array shown in full JSON |
| license.id | string | yes | See description | License identifier. | "lic_578581bd80c2a672da30fc29" |
| license.status | string | yes | enum=["active", "suspended", "revoked"] | Resulting state. | "active" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_0dd8d2e77cf580ba8b5d491f" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INVALID_STATE | 409 | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Sets an eligible license ACTIVE and clears revocation_reason. An elapsed expiration must be renewed first; an EXPIRED stored status cannot directly restore. Old tokens remain revoked; a fresh [activation](/docs/v1/start/glossary#activation) is needed. SUSPENDED-to-ACTIVE produces audit/[webhook](/docs/v1/start/glossary#webhook) license.restored. ACTIVE-to-ACTIVE and PENDING-to-ACTIVE produce audit license.active but no restoration webhook. No email is sent directly.
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. Repeating restore is not guaranteed to repeat the same event type: a formerly suspended license is now active. Updates use row locks without caller revision checks. A concurrent removal after the preliminary read yields INVALID_STATE.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Shared license-write bucket:60 per60 seconds per key across revoke, suspend, restore and renew.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh restore.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py restore.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs restore.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php restore.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- restore.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INVALID_STATE (HTTP 409) | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Renew a license
Canonical: https://licensing.kdpika.com/docs/v1/api/renew
---
title: "Renew a license"
description: "Extend license expiry without charging or changing its plan."
slug: "/docs/v1/api/renew"
section: "API reference"
order: 35
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Extend [license](/docs/v1/start/glossary#license) expiry without charging or changing its plan.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/licenses/{license}/renew
### When to use it
Extend a license expiration after your own trusted backend confirms a renewal. This operation does not charge a buyer, change the [product plan](/docs/v1/start/glossary#product-plan) or apply platform-subscription billing.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) licenses:write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
No pagination, filtering, sorting or list query parameters are implemented. Use JSON rather than relying on alternate form/query parsing.
## Request body
| Field | Type | Required | Constraints/default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| days | integer | yes | minimum=1; maximum=3650 | Days added to future expiry or current server time;1–3650. | 30 |
### Full request JSON
```json
{
"days": 30
}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"license": {
"id": "lic_578581bd80c2a672da30fc29",
"status": "active",
"expires_at": "2026-12-16T10:39:35+00:00"
},
"request_id": "req_ef518703be07a281beed3534"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| license | object | yes | See description | Renewal result. | Nested object/array shown in full JSON |
| license.id | string | yes | See description | License identifier. | "lic_578581bd80c2a672da30fc29" |
| license.status | string | yes | enum=["active", "suspended", "pending"] | Resulting state; suspended/pending remain so. | "active" |
| license.expires_at | string or null | yes | format="date-time" | Extended expiration. | "2026-12-16T10:39:35+00:00" |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_ef518703be07a281beed3534" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Adds1–3650 whole days to a future expiration, otherwise to current server time. A license with no expiration becomes time-limited. EXPIRED becomes ACTIVE; SUSPENDED and PENDING retain their state. REVOKED and CANCELLED are rejected. No revoked token is revived. Audit/[webhook](/docs/v1/start/glossary#webhook) license.renewed are emitted with the days value; no email is sent directly.
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. Every accepted repeat extends time again. Do not automatically repeat an uncertain renewal after timeout/500. Record the verified store event and previous/expected expiry durably, serialize by license, and reconcile before another extension. Row locks serialize individual updates but do not deduplicate business events. Concurrent removal after preflight can yield INVALID_REQUEST.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Shared license-write bucket:60 per60 seconds per key across revoke, suspend, restore and renew.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh renew.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py renew.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs renew.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php renew.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- renew.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INVALID_REQUEST (HTTP 422) | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Reset license devices
Canonical: https://licensing.kdpika.com/docs/v1/api/reset-devices
---
title: "Reset license devices"
description: "Release active devices and revoke their tokens."
slug: "/docs/v1/api/reset-devices"
section: "API reference"
order: 36
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/conventions", "/docs/v1/start/authentication"]
draft: false
---
Release [active devices](/docs/v1/start/glossary#active-devices) and revoke their tokens.
## What you will learn
- Send this exact operation with its required credentials and fields.
- Interpret success, refusal, side effects and retry risks.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [API conventions](/docs/v1/api/conventions) and [authentication](/docs/v1/start/authentication).
- Keep [customer](/docs/v1/start/glossary#customer) credentials private and management keys on your trusted backend.
## Method and path
POST /api/v1/[licenses](/docs/v1/start/glossary#license)/{license}/reset-devices
### When to use it
Release all active installation seats from your trusted backend after an authorized device-recovery/support request. This is not a reset of lifetime [activation](/docs/v1/start/glossary#activation) policy.
## Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with [scope](/docs/v1/start/glossary#scope) [devices](/docs/v1/start/glossary#device):write. The key fixes the [organization](/docs/v1/start/glossary#organization)/[product](/docs/v1/start/glossary#product)/environment; X-Environment cannot override it.
## Request headers
| Header | Required | Description | Example |
| --- | --- | --- | --- |
| Accept | recommended | JSON responses | application/json |
| Content-Type | for JSON body | Use the documented JSON object | application/json |
| Authorization | yes | Scoped backend-only [developer](/docs/v1/start/glossary#developer) key | Bearer YOUR_DEVELOPER_API_KEY |
## Path and query parameters
| Parameter | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| license | string | yes | Opaque license identifier. Scoped lookup returns 404; no UUID/length contract. | YOUR_LICENSE_ID |
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
```json
{}
```
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](/docs/v1/api/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.
```json
{
"success": true,
"devices_reset": true,
"request_id": "req_94a800afce76076106be37b8"
}
```
### Response fields
| Field | Type | Required | Constraints | Description | Example |
| --- | --- | --- | --- | --- | --- |
| success | boolean | yes | const=true | The operation succeeded. | true |
| devices_reset | boolean | yes | const=true | Active devices released and tokens revoked; cumulative count unchanged. | true |
| request_id | string | yes | pattern="^req_[a-f0-9]{24}$" | Request correlation ID. Replay bodies can retain an earlier ID. | "req_94a800afce76076106be37b8" |
### 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](/docs/v1/api/rate-limits).
## Error responses
| Code | HTTP | Cause | Fix | Retryable? |
| --- | --- | --- | --- | --- |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | No blind retry; apply the correction |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | No blind retry; apply the correction |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | Only safe reads after backoff; reconcile mutations |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | No blind retry; apply the correction |
| FORBIDDEN | 403 | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | No blind retry; apply the correction |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | After the 60-second window, with jitter |
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](/docs/v1/api/errors) keeps SDK-local and operational namespaces separate.
## Side effects
Marks all ACTIVE devices DEACTIVATED and revokes active tokens for the scoped license. Does not change license status, expiration or activation_count. Re-activation still obeys status, expiry and max_activations. Audit license.devices_reset and [webhook](/docs/v1/start/glossary#webhook) device.reset are emitted; no email is sent directly.
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. Repeating a reset may emit another audit/webhook even if no seats remain. The locked mutation can return INVALID_REQUEST if a concurrent soft deletion occurs after the controller's initial scoped read; an already missing license returns LICENSE_NOT_FOUND instead. No caller revision check is available.
## Rate limits
Developer pre-authentication:240 per60 seconds per IP. Reset bucket:60 per60 seconds per developer-key device-write bucket, separate from shared license-write.
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](/docs/v1/security/verify-ed25519) before granting access.
Run sh request.sh reset-devices.
### cURL
```curl
#!/bin/sh
# Purpose: inspect one Sandbox API operation; not signature verification.
# Requirements: POSIX sh, curl7.76+. Run: sh request.sh public-key
# Expected: JSON; exit0 HTTP2xx/1 HTTP rejection/2 setup or transport failure.
# KDP_* variables carry credentials/body. Output can contain newly issued secrets:
# use a private terminal only, never application/CI logs. Verify signed state separately.
set -eu
fail() { printf '%s\n' 'Check the operation and KDP_* configuration. Do not blindly retry mutations.' >&2; exit 2; }
[ "$#" -eq 1 ] || fail
name=$1
case "$name" in
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' ;;
*) fail ;;
esac
base=${KDP_BASE_URL:-https://licensing.kdpika.com/api/v1}
base=${base%/}
case "$base" in *'@'*|*'?'*|*'#'*|*' '* ) fail ;; esac
case "$base" in
https://*/api/v1) ;;
http://127.0.0.1:*/api/v1|http://localhost:*/api/v1) [ "${KDP_ALLOW_LOOPBACK:-0}" = 1 ] || fail ;;
*) fail ;;
esac
case "$path" in *'{license}'*)
id=${KDP_LICENSE_ID:?Set KDP_LICENSE_ID}
case "$id" in *[!A-Za-z0-9_-]*|'') fail ;; esac
path=$(printf '%s' "$path" | sed "s/{license}/$id/") ;;
esac
set -- --silent --show-error --fail-with-body --connect-timeout 5 --max-time 15 --request "$method" \
--header 'Accept: application/json' --header 'User-Agent: KDPikaDocsExample/1.0 curl'
case "$auth" in
developer)
case "${KDP_API_KEY:-}" in lmp_sandbox_*) ;; *) fail ;; esac
set -- "$@" --header "Authorization: Bearer $KDP_API_KEY" ;;
activation) set -- "$@" --header "Authorization: Bearer ${KDP_ACTIVATION_TOKEN:?Set KDP_ACTIVATION_TOKEN}" ;;
publicApp) set -- "$@" --header "X-App-ID: ${KDP_APP_ID:?Set KDP_APP_ID}" --header 'X-Environment: SANDBOX' ;;
esac
if [ "$name" = create-license ]; then set -- "$@" --header "Idempotency-Key: ${KDP_IDEMPOTENCY_KEY:?Set KDP_IDEMPOTENCY_KEY}"; fi
if [ "$name" = validate ]; then set -- "$@" --header "X-Installation-ID: ${KDP_INSTALLATION_ID:?Set KDP_INSTALLATION_ID}"; fi
if [ "$method" = POST ]; then
body=${KDP_BODY_JSON-'{}'}
set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
fi
# Keep the response in memory only, separating the final status line. No file,
# --location, --insecure or --verbose. The caller must still inspect JSON success.
if response=$(curl "$@" --write-out '\n%{http_code}' "$base$path"); then transport=0; else transport=$?; fi
newline='
'
status=${response##*"$newline"}
payload=${response%"$newline"*}
case "$status" in [0-9][0-9][0-9]) ;; *) fail ;; esac
if [ "$transport" -ne 0 ] && [ "$transport" -ne 22 ]; then fail; fi
printf '%s\n' "$payload"
case "$status" in 2[0-9][0-9]) exit 0 ;; esac
printf '%s\n' 'The API did not accept the request. Inspect error.code/request_id; reconcile mutations before retrying.' >&2
exit 1
```
Run python3 request.py reset-devices.
### Python
```python
"""Inspect one Sandbox API operation; transport example, not access authorization.
Requires Python3.9+ standard library. Run: python3 request.py public-key
Expected: full API JSON on stdout; exit0 on success,1 on API rejection,2 on setup/transport failure.
Credentials/body via KDP_* environment variables. Stdout may contain new secrets:
use only a private terminal, never application/CI logs. Verify signed state separately.
"""
import json
import os
import re
import sys
from urllib.error import HTTPError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
# Preserve raw UTF-8 JSON even when piped on a non-UTF-8 Windows console.
sys.stdout.reconfigure(encoding='utf-8')
OPERATIONS = json.loads(r'''{
"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"
}
}''')
def required(name):
value = os.environ.get(name, '')
if not value or '\r' in value or '\n' in value:
raise ValueError('Set a valid ' + name + '.')
return value
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self, request, file, code, message, headers, new_url):
return None
def main():
if len(sys.argv) != 2 or sys.argv[1] not in OPERATIONS:
raise ValueError('Choose an operation: ' + ', '.join(OPERATIONS))
operation = OPERATIONS[sys.argv[1]]
base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').rstrip('/')
parsed = urlsplit(base)
loopback = parsed.scheme == 'http' and parsed.hostname in ('127.0.0.1', 'localhost') and os.environ.get('KDP_ALLOW_LOOPBACK') == '1'
if not parsed.hostname or (parsed.scheme != 'https' and not loopback) or parsed.username or parsed.password or parsed.query or parsed.fragment or parsed.path != '/api/v1':
raise ValueError('Use an HTTPS API base URL ending in /api/v1. Loopback HTTP needs explicit test opt-in.')
path = operation['path']
if '{license}' in path:
identifier = required('KDP_LICENSE_ID')
if not re.fullmatch(r'[A-Za-z0-9_-]+', identifier):
raise ValueError('This sample accepts the simple license IDs issued by the platform.')
path = path.replace('{license}', identifier)
headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Python'}
if operation['auth'] == 'developer':
key = required('KDP_API_KEY')
if not key.startswith('lmp_sandbox_'):
raise ValueError('Use a Sandbox developer API key with this example.')
headers['Authorization'] = 'Bearer ' + key
elif operation['auth'] == 'activation':
headers['Authorization'] = 'Bearer ' + required('KDP_ACTIVATION_TOKEN')
elif operation['auth'] == 'publicApp':
headers.update({'X-App-ID': required('KDP_APP_ID'), 'X-Environment': 'SANDBOX'})
if sys.argv[1] == 'create-license':
headers['Idempotency-Key'] = required('KDP_IDEMPOTENCY_KEY')
if sys.argv[1] == 'validate':
headers['X-Installation-ID'] = required('KDP_INSTALLATION_ID')
body = None
if operation['method'] == 'POST':
source = os.environ.get('KDP_BODY_JSON', '{}')
if not isinstance(json.loads(source), dict):
raise ValueError('KDP_BODY_JSON must be a JSON object.')
body = source.encode('utf-8')
headers['Content-Type'] = 'application/json'
request = Request(base + path, data=body, headers=headers, method=operation['method'])
try:
response = build_opener(NoRedirect()).open(request, timeout=15)
except HTTPError as error:
response = error
raw = response.read().decode('utf-8')
result = json.loads(raw)
sys.stdout.write(raw + '\n')
if not (200 <= response.status < 300 and result.get('success') is True):
sys.stderr.write('The API rejected this request. Check error.code and request_id; do not blindly retry a mutation.\n')
return 1
return 0
if __name__ == '__main__':
try:
sys.exit(main())
except ValueError as error:
# Configuration errors never interpolate credential values.
sys.stderr.write(str(error) if str(error).startswith(('Set a valid KDP_', 'Choose an operation:', 'Use ', 'This sample ', 'KDP_BODY_JSON ')) else 'The response or request was not valid JSON.')
sys.stderr.write('\n')
sys.exit(2)
except Exception:
sys.stderr.write('Request unavailable. Check connectivity; reconcile uncertain mutations before retrying.\n')
sys.exit(2)
```
Run node request.mjs reset-devices.
### JavaScript (Node)
```javascript
// Purpose: inspect one Sandbox API operation; not an entitlement verifier.
// Requirements: Node18.20+ native fetch, trusted server/Electron main process.
// Run: node request.mjs public-key. Expected: JSON; exit0 success/1 rejection/2 transport or setup.
// Credentials/body use KDP_* environment variables. Output can contain new secrets.
// Use a private terminal, never application/CI logs. Verify signed state separately.
const operations = Object.freeze({
"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"
}
});
function required(name) {
const value = process.env[name];
if (!value || /[\r\n]/.test(value)) throw new Error(`Set a valid ${name}.`);
return value;
}
async function main() {
const name = process.argv[2];
if (process.argv.length !== 3 || !Object.hasOwn(operations, name)) throw new Error('Choose a documented operation.');
const operation = operations[name];
const base = (process.env.KDP_BASE_URL || 'https://licensing.kdpika.com/api/v1').replace(/\/+$/, '');
const url = new URL(base);
const loopback = url.protocol === 'http:' && ['127.0.0.1', 'localhost'].includes(url.hostname) && process.env.KDP_ALLOW_LOOPBACK === '1';
if ((url.protocol !== 'https:' && !loopback) || url.username || url.password || url.search || url.hash || url.pathname !== '/api/v1') throw new Error('Use an HTTPS API URL ending in /api/v1; loopback HTTP requires explicit test opt-in.');
let path = operation.path;
if (path.includes('{license}')) {
const id = required('KDP_LICENSE_ID');
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new Error('Use the simple license ID issued by this platform with this sample.');
path = path.replace('{license}', id);
}
const headers = {'Accept': 'application/json', 'User-Agent': 'KDPikaDocsExample/1.0 Node'};
if (operation.auth === 'developer') {
const key = required('KDP_API_KEY');
if (!key.startsWith('lmp_sandbox_')) throw new Error('Use a Sandbox developer key.');
headers.Authorization = `Bearer ${key}`;
} else if (operation.auth === 'activation') headers.Authorization = `Bearer ${required('KDP_ACTIVATION_TOKEN')}`;
else if (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');
let body;
if (operation.method === 'POST') {
body = process.env.KDP_BODY_JSON || '{}';
const value = JSON.parse(body);
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('KDP_BODY_JSON must be a JSON object.');
headers['Content-Type'] = 'application/json';
}
const response = await fetch(base + path, {method: operation.method, headers, body, redirect: 'error', signal: AbortSignal.timeout(15000)});
const raw = await response.text();
const result = JSON.parse(raw);
process.stdout.write(raw + '\n');
if (!response.ok || result.success !== true) {
process.stderr.write('The API rejected this request. Inspect error.code and request_id; reconcile mutations before retrying.\n');
return 1;
}
return 0;
}
main().then(code => {process.exitCode = code;}).catch(() => {
process.stderr.write('Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations.\n');
process.exitCode = 2;
});
```
Run php request.php reset-devices.
### PHP
```php
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);
}
```
Run dotnet run --project ApiExample.csproj -- reset-devices.
### C#
```csharp
// Purpose: inspect one Sandbox API operation; transport only, not feature access.
// Requirements: .NET7+ console project. Run: dotnet run -- public-key
// Expected: JSON; exit0 success/1 API rejection/2 setup or transport failure.
// KDP_* environment variables carry credentials/body. Stdout can contain new secrets:
// use a private terminal, never application/CI logs. Verify signed state separately.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
Console.OutputEncoding = new UTF8Encoding(false);
const string Contract = """
{
"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"
}
}
""";
try
{
using var operations = JsonDocument.Parse(Contract);
if (args.Length != 1 || !operations.RootElement.TryGetProperty(args[0], out var operation)) throw new Exception("Invalid operation.");
var name = args[0];
string Required(string key)
{
var value = Environment.GetEnvironmentVariable(key);
if (String.IsNullOrEmpty(value) || value.Contains('\r') || value.Contains('\n')) throw new Exception("Missing configuration.");
return value;
}
var root = (Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com/api/v1").TrimEnd('/');
var uri = new Uri(root);
var loopback = uri.Scheme == "http" && (uri.Host == "127.0.0.1" || uri.Host == "localhost") && Environment.GetEnvironmentVariable("KDP_ALLOW_LOOPBACK") == "1";
if ((uri.Scheme != "https" && !loopback) || uri.UserInfo != "" || uri.Query != "" || uri.Fragment != "" || uri.AbsolutePath != "/api/v1") throw new Exception("Invalid base URL.");
var path = operation.GetProperty("path").GetString()!;
if (path.Contains("{license}"))
{
var identifier = Required("KDP_LICENSE_ID");
if (!Regex.IsMatch(identifier, "^[A-Za-z0-9_-]+$")) throw new Exception("Unsupported sample identifier.");
path = path.Replace("{license}", identifier);
}
using var handler = new HttpClientHandler {AllowAutoRedirect = false};
using var client = new HttpClient(handler) {Timeout = TimeSpan.FromSeconds(15)};
using var request = new HttpRequestMessage(new HttpMethod(operation.GetProperty("method").GetString()!), root + path);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.UserAgent.ParseAdd("KDPikaDocsExample/1.0 CSharp");
var auth = operation.GetProperty("auth").GetString();
if (auth == "developer")
{
var key = Required("KDP_API_KEY");
if (!key.StartsWith("lmp_sandbox_", StringComparison.Ordinal)) throw new Exception("Sandbox key required.");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
}
else if (auth == "activation") request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Required("KDP_ACTIVATION_TOKEN"));
else if (auth == "publicApp")
{
request.Headers.Add("X-App-ID", Required("KDP_APP_ID")); request.Headers.Add("X-Environment", "SANDBOX");
}
if (name == "create-license") request.Headers.Add("Idempotency-Key", Required("KDP_IDEMPOTENCY_KEY"));
if (name == "validate") request.Headers.Add("X-Installation-ID", Required("KDP_INSTALLATION_ID"));
if (request.Method == HttpMethod.Post)
{
var body = Environment.GetEnvironmentVariable("KDP_BODY_JSON") ?? "{}";
using var parsedBody = JsonDocument.Parse(body);
if (parsedBody.RootElement.ValueKind != JsonValueKind.Object) throw new Exception("JSON object required.");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await client.SendAsync(request);
var raw = await response.Content.ReadAsStringAsync();
using var result = JsonDocument.Parse(raw);
Console.WriteLine(raw);
if (!response.IsSuccessStatusCode || !result.RootElement.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
{
Console.Error.WriteLine("The API rejected this request. Inspect error.code/request_id; reconcile mutations before retrying."); return 1;
}
return 0;
}
catch (Exception)
{
Console.Error.WriteLine("Request unavailable or configuration invalid. Check KDP_* settings/connectivity; do not blindly retry mutations."); return 2;
}
```
```csharp
Exenet7.0enableenable
```
## Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and [request IDs](/docs/v1/start/glossary#request-id); a successful transport or unsigned management response does not grant a [feature](/docs/v1/start/glossary#feature).
## Common mistakes
- Mixing [Public App IDs](/docs/v1/start/glossary#public-app-id), 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](/docs/v1/start/glossary#license-key) or [activation token](/docs/v1/start/glossary#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
| Symptom | Cause | Action |
| --- | --- | --- |
| LICENSE_NOT_FOUND (HTTP 404) | Management scoped license absent | Correct org/product/environment/ID |
| INVALID_REQUEST (HTTP 422) | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry |
| INTERNAL_ERROR (HTTP 500) | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id |
| INVALID_API_KEY (HTTP 401) | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key |
| FORBIDDEN (HTTP 403) | Developer scope missing; checkout account role not OWNER/ADMIN | Use authorized scope/role |
| RATE_LIMITED (HTTP 429) | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter |
## Next steps
Read [idempotency](/docs/v1/api/idempotency) and [signed responses](/docs/v1/security/signed-responses) before integrating a purchase-to-license workflow.
## Related
- [API conventions](/docs/v1/api/conventions)
- [Errors](/docs/v1/api/errors)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Errors and safe user messages
Canonical: https://licensing.kdpika.com/docs/v1/api/errors
---
title: "Errors and safe user messages"
description: "Handle every current licensing error, distinguish SDK-local and operational codes, and give users a safe next step."
slug: "/docs/v1/api/errors"
section: "API reference"
order: 37
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Use error.code for decisions and a clear, non-technical message for users; never infer a [license](/docs/v1/start/glossary#license) from an HTTP status alone.
## What you will learn
- Recognize all current API/router codes and the separate SDK/web namespaces.
- Handle refusal without leaking credentials or enabling [features](/docs/v1/start/glossary#feature).
- Use [request IDs](/docs/v1/start/glossary#request-id) with support.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Error envelope
Licensing API failures use the actual non-2xx HTTP status and this shape. This is a real invalid-token capture with a synthetic request ID:
```json
{"success":false,"error":{"code":"INVALID_TOKEN","message":"The activation token is invalid."},"request_id":"req_000000000000000000000000"}
```
SDK result objects instead expose valid/Valid, error_code/errorCode/ErrorCode, message and request_id/requestId/RequestId. A local result has no HTTP response. Operational/billing endpoints can use a string error field without this envelope. Do not apply their status codes or Retry-After headers to licensing requests.
A known code can occur in more than one namespace: INVALID_LICENSE and LICENSE_EXPIRED can also be SDK-local, while FORBIDDEN/RATE_LIMITED/NOT_FOUND are reused by unrelated web routes. Preserve the operation and source as well as the code.
## Licensing API and router
| Code | HTTP or local | Meaning / common causes | Fix and retry rule | Where it is returned | Suggested end-user message |
| --- | --- | --- | --- | --- | --- |
| ACTIVATION_LIMIT_REACHED | 409 | New/inactive installation while activation_count >= max_activations | [Developer](/docs/v1/start/glossary#developer) adjusts policy; reset-devices does not zero count | activate | This license has reached its [device](/docs/v1/start/glossary#device) or [activation](/docs/v1/start/glossary#activation) limit. Contact the software developer for help. |
| APP_VERSION_NOT_SUPPORTED | 403 | Nonempty activation app_version outside configured range | Use supported version | activate | This version of the application is not covered by your license. Install a supported version. |
| DEVICE_LIMIT_REACHED | 409 | New/inactive installation while active seats >= license.max_devices | Deactivate/reset a seat | activate | This license has reached its device or activation limit. Contact the software developer for help. |
| DEVICE_MISMATCH | 403 | Provided [installation ID](/docs/v1/start/glossary#installation-id) does not match token-bound HMAC hash | Use original installation; do not copy token | validate | Please activate this installation again with your license details. |
| DEVICE_NOT_AUTHORIZED | 403 | Token is active but associated device is not ACTIVE | Reactivation if eligible | validate | Please activate this installation again with your license details. |
| FORBIDDEN | 403 | Developer [scope](/docs/v1/start/glossary#scope) missing; checkout account role not OWNER/ADMIN | Use authorized scope/role | create, get, revoke, suspend, restore, renew, reset-devices | Your account connection needs attention. Please contact the software developer. |
| IDEMPOTENCY_CONFLICT | 409 | [Organization](/docs/v1/start/glossary#organization) key reused with different decoded-body hash (member order matters) | Use same body; new key only for new operation | create | This request differs from an earlier request. Please contact support before trying again. |
| INTERNAL_ERROR | 500 | Uncaught exception at global boundary | Safe retry only for idempotent operation; preserve request_id | public-key, create, activate, validate, deactivate, get, revoke, suspend, restore, renew, reset-devices | We could not complete this request. Please try again later or contact support if it continues. |
| INVALID_API_KEY | 401 | Missing/invalid/revoked key or prohibited subscription/environment | Replace/configure key | create, get, revoke, suspend, restore, renew, reset-devices | Your account connection needs attention. Please contact the software developer. |
| INVALID_LICENSE | 404 | Activation key/[product](/docs/v1/start/glossary#product)/environment/[customer](/docs/v1/start/glossary#customer) email mismatch; SDK empty key local error has no HTTP | Correct details | activate | We could not activate these license details. Check your key and email, then try again. |
| INVALID_REQUEST | 422 | Input constraints or entitlement/resource/renewal domain rejection | Correct input/plan; not blind retry | create, activate, validate, renew, reset-devices | Some information or account settings need attention. Check your details or contact support. |
| INVALID_STATE | 409 | Forbidden state transition or restore with elapsed expiry | Resolve state/renew first | revoke, suspend, restore | This change is not available for the current license. Please contact the software developer. |
| INVALID_TOKEN | 401 | Missing/non-act_/unknown/revoked/deactivated [activation token](/docs/v1/start/glossary#activation-token) | Reactivate eligible installation | validate, deactivate | Please activate this installation again with your license details. |
| LICENSE_CANCELLED | 403 | License CANCELLED and no elapsed deadline taking precedence | Do not retry; new eligible license | activate, validate | This license is no longer available. Please contact the software developer. |
| LICENSE_EXPIRED | 403 | Elapsed expires_at before state check; or EXPIRED state; SDK local expiry has no HTTP | Renew eligible license | activate, validate | Your license has expired. Contact the software developer to renew it. |
| LICENSE_NOT_FOUND | 404 | Management scoped license absent | Correct org/product/environment/ID | get, revoke, suspend, restore, renew, reset-devices | We could not find the requested license or plan. Please contact the software developer. |
| LICENSE_PENDING | 403 | License PENDING and no elapsed deadline taking precedence | Wait for authorized activation | activate, validate | This license is not ready to use. Please contact the software developer. |
| LICENSE_REVOKED | 403 | License REVOKED and no elapsed deadline taking precedence | Do not retry; contact developer | activate, validate | This license is no longer available. Please contact the software developer. |
| LICENSE_SUSPENDED | 403 | License SUSPENDED and no elapsed deadline taking precedence | Restore then reactivate | activate, validate | This license is not ready to use. Please contact the software developer. |
| NOT_FOUND | 404 | Unregistered API route/wrong method; or protected operational guard mismatch | Correct path/method/token; no blind retry | Global API router (unknown path or wrong method); separate guarded operational routes | This request is not available. Please contact support. |
| PLAN_NOT_FOUND | 404 | Create product/plan does not resolve within active API-key product | Correct scoped IDs | create | We could not find the requested license or plan. Please contact the software developer. |
| RATE_LIMITED | 429 | Bucket exhausted; wait for window reset; no licensing Retry-After header | After window; apply jitter | public-key, create, get, revoke, suspend, restore, renew, reset-devices, activate, validate | Too many requests were sent. Please wait a minute before trying again. |
## SDK-local and transport results
| Code | HTTP or local | Meaning / common causes | Fix and retry rule | Where it is returned | Suggested end-user message |
| --- | --- | --- | --- | --- | --- |
| CLOCK_ROLLBACK_DETECTED | local | Clock is >300 seconds behind saved/server trusted checkpoint | Correct clock then validate | No API-v1 emitter; SDK/web/operational namespace | Your device clock needs attention. Set the correct date and time, then connect to check your license. |
| HTTP_ERROR | local / inherited HTTP | Non-success remote response without structured licensing error; fallback SDK code | Inspect actual status; no automatic blind retry | No API-v1 emitter; SDK/web/operational namespace | We could not complete this request. Please try again later or contact support if it continues. |
| INVALID_RESPONSE | local | Malformed JSON or missing token/envelope in SDK response | Reconnect/contact support | No API-v1 emitter; SDK/web/operational namespace | We could not safely confirm your license. Connect again or contact the software developer. |
| LICENSE_NOT_ACTIVE | local | SDK verified payload status is not active | Check account/license state | No API-v1 emitter; SDK/web/operational namespace | This license is not ready to use. Please contact the software developer. |
| NETWORK_ERROR | local | SDK activation/deactivation transport failure; check may instead use offline cache | Retry connectivity, do not bypass verification | No API-v1 emitter; SDK/web/operational namespace | Connect to the internet to check your license, then try again. |
| NOT_ACTIVATED | local | SDK has no stored token for check/deactivate | Activate | No API-v1 emitter; SDK/web/operational namespace | Please activate this installation again with your license details. |
| OFFLINE_CACHE_INVALID | local | Cached envelope cannot verify; SDK removes it | Online validation; investigate tampering or known canonical defect | No API-v1 emitter; SDK/web/operational namespace | We could not safely confirm your license. Connect again or contact the software developer. |
| OFFLINE_CACHE_MISSING | local | Transport fallback without stored signed cache | Online validation | No API-v1 emitter; SDK/web/operational namespace | Connect to the internet to check your license, then try again. |
| OFFLINE_GRACE_EXPIRED | local | Cache/[offline grace](/docs/v1/start/glossary#offline-grace) missing or now beyond signed deadline | Online validation | No API-v1 emitter; SDK/web/operational namespace | Connect to the internet to check your license, then try again. |
| SIGNATURE_INVALID | local | SDK signature/envelope/key/product/environment/installation verification exception | Fail closed; investigate key/canonicalization | No API-v1 emitter; SDK/web/operational namespace | We could not safely confirm your license. Connect again or contact the software developer. |
## Other web and operational namespaces
| Code | HTTP or local | Meaning / common causes | Fix and retry rule | Where it is returned | Suggested end-user message |
| --- | --- | --- | --- | --- | --- |
| BACKUP_FAILED | 500 | Backup exception | Operator investigates | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| CHECKOUT_UNAVAILABLE | 409 | Checkout creation DomainException | Resolve account/order then retry | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| CRON_FAILED | 500 | Cron exception | Operator investigates | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INSTALLER_LOCKED | 410 | Installer lock exists | No public retry; installation already locked | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INSTALL_FAILED | 500 | Installer exception | Operator investigates; not public API | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INVALID_CSRF | 419 | JSON request through CSRF middleware has invalid session token; not currently API-v1 middleware | Refresh session token | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INVALID_EVENT | 400 | Stripe event domain validation failure | Investigate account/event binding | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INVALID_JSON | 400 | Stripe [webhook](/docs/v1/start/glossary#webhook) malformed JSON/provider unexpected value | Correct event; do not retry identical malformed payload | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| INVALID_SIGNATURE | 400 | Stripe webhook missing secret or invalid signature | Correct configuration; reject untrusted event | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| METHOD_NOT_ALLOWED | 405 | Installer called with non-POST | Operational only; correct method | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| MIGRATION_FAILED | 500 | Migration exception | Operator investigates | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| PROCESSING_FAILED | 500 | Stripe event unexpected processing failure | Provider [delivery](/docs/v1/start/glossary#delivery) retry is appropriate | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| PROVISION_FAILED | 500 | Admin provisioning exception | Operator investigates | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| SDK_NOT_FOUND | 404 | Authenticated download requested unsupported language | Correct language | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| SDK_UNAVAILABLE | 503 | SDK directory/archive temporary failure | Retry later | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
| STRIPE_UNAVAILABLE | 503 | Checkout creation unexpected exception | Retry after 10s | No API-v1 emitter; SDK/web/operational namespace | This service action could not be completed. Please contact support at hi@kdpika.com. |
## Retry decisions
RATE_LIMITED: wait a full 60-second licensing window plus jitter when no previous reset time is available. Authentication/input/state/signature errors require correction, not blind retries. Network/500 errors after mutations are uncertain outcomes: reconcile creation/renewal before another write. HTTP_ERROR is a fallback, not proof that an upstream error is safe to retry. Unknown future codes fail closed.
PRODUCT_NOT_FOUND and DEVICE_NOT_FOUND appeared in older documentation but have no current API-v1 emitter. They are not supported error promises.
## Safe handling in every official SDK language
These complete programs call the unchanged SDK check() equivalent. An invalid result never enables a feature. Messages/actions come from one adjacent policy.json, so the four languages use the same text. The full self-test mode uses actual result models with synthetic inputs; it does not claim to reproduce48 runtime emitters. The separate API contract tests reproduce the 22 licensing/router codes.
The exact shared policy.json is in the [complete example archive](/docs/downloads/api-examples.zip); its48 messages/actions match the full table above. Keep that dependency beside the executable sample instead of inventing a second mapping.
:::tabs
:::tab python Python
```python
"""Safe SDK error UX, Python3.9+ / official Python SDK1.0.0.
Run: python3 python.py (KDP_APP_ID, KDP_STATE_DIR, optional KDP_BASE_URL origin).
Self-test: python3 python.py --self-test < cases.json; real result model, no network.
Output: valid/message/action/request_id only; never raw payload, key or token.
"""
import json
import os
import re
import sys
from pathlib import Path
from license_sdk import LicenseClient
from license_sdk.models import LicenseResult
POLICY=json.loads(Path(__file__).with_name('policy.json').read_text())
def explain(result):
# Check valid first. A feature helper alone is not an access check.
entry={'message':'Your license is ready.','action':'continue'} if result.valid else POLICY['codes'].get(result.error_code,POLICY['fallback'])
request_id=result.request_id if isinstance(result.request_id,str) and re.fullmatch(r'req_[a-f0-9]{24}',result.request_id) else None
return {'valid':result.valid,'message':entry['message'],'action':entry['action'],'request_id':request_id}
def main():
if sys.argv[1:]==['--self-test']:
rows=json.load(sys.stdin)
for row in rows:
result=LicenseResult(valid=row['valid'],source='synthetic contract',error_code=row.get('code'),request_id=row.get('request_id'))
print(json.dumps(explain(result)))
return
if len(sys.argv)!=1:raise ValueError('Unknown action')
client=LicenseClient(product=os.environ['KDP_APP_ID'],environment='SANDBOX',
base_url=os.getenv('KDP_BASE_URL','https://licensing.kdpika.com'),storage_dir=Path(os.environ['KDP_STATE_DIR']),prefer_keyring=False)
result=client.check(force_online=False)
print(json.dumps(explain(result)))
if __name__=='__main__':
try:main()
except Exception:
print('The license check is unavailable. Check configuration or contact the software developer.',file=sys.stderr);sys.exit(2)
```
:::
:::tab javascript JavaScript
```javascript
// Safe SDK error UX. Node18.20+, official JavaScript SDK1.0.0, trusted runtime only.
// Run: node javascript.mjs; KDP_APP_ID/KDP_STATE_DIR, optional KDP_BASE_URL site origin.
// --self-test reads synthetic cases from stdin and uses actual SDK result objects.
// Output: safe UX/request ID only; no raw payload, key or token.
import fs from 'node:fs';
import {LicenseClient,LicenseResult} from '@kdpika/license-sdk';
const policy=JSON.parse(fs.readFileSync(new URL('./policy.json',import.meta.url),'utf8'));
export function explain(result){
const entry=result.valid?{message:'Your license is ready.',action:'continue'}:policy.codes[result.errorCode]||policy.fallback;
const requestId=typeof result.requestId==='string'&&/^req_[a-f0-9]{24}$/.test(result.requestId)?result.requestId:null;
return {valid:result.valid,message:entry.message,action:entry.action,request_id:requestId};
}
try {
if(process.argv.slice(2).join(' ')==='--self-test'){
const rows=JSON.parse(fs.readFileSync(0,'utf8'));
for(const row of rows)console.log(JSON.stringify(explain(new LicenseResult({valid:row.valid,source:'synthetic contract',errorCode:row.code,requestId:row.request_id}))));
}else{
if(process.argv.length!==2)throw Error();
const client=new LicenseClient({product:process.env.KDP_APP_ID,environment:'SANDBOX',baseUrl:process.env.KDP_BASE_URL||'https://licensing.kdpika.com',storageDir:process.env.KDP_STATE_DIR});
console.log(JSON.stringify(explain(await client.check(false))));
}
}catch{console.error('The license check is unavailable. Check configuration or contact the software developer.');process.exitCode=2;}
```
:::
:::tab php PHP
```php
valid?['message'=>'Your license is ready.','action'=>'continue']:($policy['codes'][$result->errorCode??'']??$policy['fallback']);
$id=$result->requestId!==null&&preg_match('/^req_[a-f0-9]{24}$/D',$result->requestId)?$result->requestId:null;
return ['valid'=>$result->valid,'message'=>$entry['message'],'action'=>$entry['action'],'request_id'=>$id];
}
if(($argv[1]??'')==='--self-test'&&$argc===2){
foreach(json_decode((string)stream_get_contents(STDIN),true,64,JSON_THROW_ON_ERROR) as $row){
$result=new KDPika\LicenseSdk\LicenseResult(valid:$row['valid'],source:'synthetic contract',errorCode:$row['code']??null,requestId:$row['request_id']??null);
echo json_encode(explain($result,$policy),JSON_THROW_ON_ERROR).PHP_EOL;
}
}else{
if($argc!==1)throw new RuntimeException('Unknown action');
$client=new KDPika\LicenseSdk\LicenseClient(product:(string)getenv('KDP_APP_ID'),environment:'SANDBOX',baseUrl:getenv('KDP_BASE_URL')?:'https://licensing.kdpika.com',storageDir:(string)getenv('KDP_STATE_DIR'));
echo json_encode(explain($client->check(false),$policy),JSON_THROW_ON_ERROR).PHP_EOL;
}
}catch(Throwable){fwrite(STDERR,"The license check is unavailable. Check configuration or contact the software developer.\n");exit(2);}
```
:::
:::tab csharp C#
```csharp
// Safe SDK error UX. .NET7+, official C# SDK1.0.0 project/package reference.
// Run: dotnet run --project Errors.csproj; KDP_APP_ID/KDP_STATE_DIR, optional site origin KDP_BASE_URL.
// --self-test reads synthetic cases from stdin; actual SDK result model, no network.
// Output safe message/action/request_id only; never raw payload/key/token.
using System.Text.Json;
using System.Text.RegularExpressions;
using KDPika.LicenseSdk;
try
{
using var policy=JsonDocument.Parse(File.ReadAllText(Path.Combine(AppContext.BaseDirectory,"policy.json")));
object Explain(LicenseResult result)
{
var found=result.ErrorCode is not null&&policy.RootElement.GetProperty("codes").TryGetProperty(result.ErrorCode,out _);
var entry=found?policy.RootElement.GetProperty("codes").GetProperty(result.ErrorCode!):policy.RootElement.GetProperty("fallback");
var message=result.Valid?"Your license is ready.":entry.GetProperty("message").GetString();
var action=result.Valid?"continue":entry.GetProperty("action").GetString();
var id=result.RequestId is not null&&Regex.IsMatch(result.RequestId,"^req_[a-f0-9]{24}$")?result.RequestId:null;
return new {valid=result.Valid,message,action,request_id=id};
}
if(args.Length==1&&args[0]=="--self-test")
{
using var rows=JsonDocument.Parse(Console.In.ReadToEnd());
foreach(var row in rows.RootElement.EnumerateArray())
{
var result=new LicenseResult(row.GetProperty("valid").GetBoolean(),"synthetic contract",default,
row.TryGetProperty("code",out var code)?code.GetString():null,
RequestId:row.TryGetProperty("request_id",out var requestId)?requestId.GetString():null);
Console.WriteLine(JsonSerializer.Serialize(Explain(result)));
}
}
else
{
if(args.Length!=0)throw new Exception("Unknown action");
var client=new LicenseClient(product:Environment.GetEnvironmentVariable("KDP_APP_ID")!,environment:"SANDBOX",
baseUrl:Environment.GetEnvironmentVariable("KDP_BASE_URL")??"https://licensing.kdpika.com",
storageDir:Environment.GetEnvironmentVariable("KDP_STATE_DIR"));
Console.WriteLine(JsonSerializer.Serialize(Explain(await client.CheckAsync(false))));
}
return 0;
}
catch(Exception)
{
Console.Error.WriteLine("The license check is unavailable. Check configuration or contact the software developer.");return 2;
}
```
```xml
Exenet7.0enableenable
```
:::
:::
Install [official SDK](/docs/v1/start/glossary#official-sdk) 1.0.0 as in the [quickstart](/docs/v1/start/quickstart). Python requires 3.9+; PHP requires 8.2+ for the actual readonly result class; Node requires 18.20+ for these examples; C# targets .NET 7+. Keep policy.json beside each sample. For C#, the included project references the SDK in this repository; when using a downloaded SDK, set ProjectReference to that extracted SDK project, and keep policy.json copied to output. Run each file with no arguments to check the configured Sandbox installation. Supply KDP_APP_ID, KDP_STATE_DIR and optional KDP_BASE_URL as the site origin, not /api/v1. PHP additionally uses KDP_SDK_DIR. Node resolves the locally installed @kdpika/license-sdk.
## Request IDs and support
Record method, path, UTC time, actual HTTP status and current X-Request-ID. If a create replay body has a different request_id, retain both. SDK-local results can have no request ID; do not manufacture one. Send the IDs and a short description to hi@kdpika.com, not raw keys, tokens, customer payloads or unredacted screenshots.
Show a support reference only if it has the expected req_ correlation format. A reference is not authentication or an access grant. The sample does not render raw server error.message into HTML.
## Expected result
Your application denies invalid results, shows the next step, and retains safe correlation details without credentials.
## Common mistakes
- Calling a feature helper without checking result.valid/Valid first.
- Treating SDK-local errors as an HTTP 500 or retrying every failure.
- Displaying internal exception text or sending private payloads to support.
## Security notes
Fail closed on unknown/signature/cache/clock errors. A friendly message must never turn a refusal into success. The policy is client UX, not new server behavior.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| No request ID | Local setup/cache/transport path | Record the app version/time/code; do not invent a server ID |
| Code not in this table | New server/client version or unrelated route | Fail closed, preserve safe metadata and contact support |
| Error message changed | Human wording is not a stable machine contract | Branch on code and namespace, not English text |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Rate limits and bounded retries
Canonical: https://licensing.kdpika.com/docs/v1/api/rate-limits
---
title: "Rate limits and bounded retries"
description: "Use exact licensing API windows and headers, avoid request loops, and distinguish rate limits from monthly usage allowances."
slug: "/docs/v1/api/rate-limits"
section: "API reference"
order: 38
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Throttle by the operation you call and retry only when the operation is safe to repeat.
## What you will learn
- Read every current licensing rate bucket.
- Apply bounded backoff with jitter without assuming missing headers.
- Reduce repeated validation and distinguish advisory usage from hard capacity checks.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Current licensing limits
| Surface | Bucket [scope](/docs/v1/start/glossary#scope) | Allowance / window | Headers |
| --- | --- | --- | --- |
| Public key | IP | 60 requests /60 seconds | Remaining/Reset on success |
| [Activation](/docs/v1/start/glossary#activation) | SHA256(IP + App ID + supplied key text) | 20 /60 seconds | Remaining/Reset on success |
| Validation | SHA256(IP + [activation token](/docs/v1/start/glossary#activation-token)) | 120 /60 seconds | Remaining/Reset on success |
| [Developer](/docs/v1/start/glossary#developer) pre-authentication | IP | 240 /60 seconds | Not reported separately |
| Create [license](/docs/v1/start/glossary#license) | Developer key ID + license-create | 60 /60 seconds | Remaining/Reset on success |
| Read license | Developer key ID + license-read | 180 /60 seconds | Remaining/Reset on success |
| Revoke/suspend/restore/renew | Developer key ID + shared license-write | 60 /60 seconds shared | Remaining/Reset on success |
| Reset [devices](/docs/v1/start/glossary#device) | Developer key ID + device-write | 60 /60 seconds | Remaining/Reset on success |
| Deactivate | No dedicated limiter in current handler | No fixed allowance can be promised | X-Request-ID only |
These are fixed windows starting at the first hit after expiration, not sliding windows. The limiter increments every reached hit, even when business validation later rejects it. Shared hosting processes coordinate file buckets with locks. Client IP is REMOTE_ADDR, not an arbitrary client-supplied forwarding header. Early input/authentication failures can reject before later buckets are reached.
Management requests first consume the 240/IP authentication bucket, then the key/action bucket after authentication/scope success. The status/renew actions share one bucket. Creating a second process or changing API keys is not a way to evade a shared IP limit. Public key caching reduces that public load.
## Exact headers
| Header | When present | Interpretation |
| --- | --- | --- |
| X-Request-ID | Routed success/error | Current correlation ID |
| X-RateLimit-Remaining | Successful limited operation | Integer remaining in the reached operation bucket |
| X-RateLimit-Reset | Successful limited operation | Unix seconds, not milliseconds or ISO text |
| Retry-After | Not added by licensing errors | Do not assume a retry delay from a different billing route |
| X-RateLimit-Limit | Not implemented by these handlers | Use the documented bucket table |
A 429 licensing response omits Remaining/Reset/Retry-After. If you saved a reset time from a preceding success, use it conservatively with jitter and sane bounds. Otherwise wait the full 60-second window plus jitter; do not poll each second. Host/WAF throttles may have other formats and are not part of this PHP licensing contract.
## Bounded safe-read retry example
This full Python 3.9+ example retrieves only the public key. It performs at most four attempts, uses a15-second HTTP timeout, does not follow redirects, and never turns off TLS verification. It retries transport failures and selected transient HTTP statuses. A 429 waits60–61 seconds; other retry delays use1,2,4 seconds plus0–1 second jitter. Explicit401/403/404/409/422 and redirects are not retried. Malformed JSON does not become an access grant.
```python
"""Bounded retry for a safe read, Python3.9+ standard library.
Run: python3 backoff.py; optional KDP_BASE_URL site API base (HTTPS only).
Expected: JSON public key response; no retry is offered for creation/renewal.
--self-test exercises actual retry algorithm with injected clocks/transports,
not real API failures. No credentials/body/payload logging.
"""
import json
import os
import random
import sys
import time
from urllib.error import HTTPError, URLError
from urllib.parse import urlsplit
from urllib.request import HTTPRedirectHandler, Request, build_opener
class NoRedirect(HTTPRedirectHandler):
def redirect_request(self,*args):return None
def read_with_retry(send,*,attempts=4,wait=time.sleep,random_value=random.random):
if not 1<=attempts<=4:raise ValueError('Choose1–4 attempts.')
for attempt in range(attempts):
try:status,headers,body=send()
except (TimeoutError,URLError,OSError):status,headers,body=0,{},None
if 200<=status<300:return status,body
# A redirect or explicit authentication/input rejection is not transient.
if status not in [0,408,429,500,502,503,504] or attempt==attempts-1:return status,body
# Licensing429 has no Retry-After. Wait a full window instead of tight loops.
base=60.0 if status==429 else min(2.0**attempt,8.0)
jitter=max(0.0,min(1.0,float(random_value())))
wait(base+jitter)
raise RuntimeError('Unreachable retry state')
def send_public_key():
base=os.getenv('KDP_BASE_URL','https://licensing.kdpika.com/api/v1').rstrip('/')
uri=urlsplit(base)
local=uri.scheme=='http' and uri.hostname in ['localhost','127.0.0.1'] and os.getenv('KDP_ALLOW_LOOPBACK')=='1'
if not uri.hostname or (uri.scheme!='https' and not local) or uri.username or uri.password or uri.query or uri.fragment or uri.path!='/api/v1':raise ValueError('Invalid HTTPS API base URL.')
request=Request(base+'/public-key',headers={'Accept':'application/json','User-Agent':'KDPikaDocsBackoff/1.0'})
try:response=build_opener(NoRedirect()).open(request,timeout=15)
except HTTPError as error:response=error
return response.status,dict(response.headers),json.loads(response.read())
def self_test():
tests=[([429,200],[60.25]),([503,502,200],[1.25,2.25]),([401],[]),([422],[]),([307],[]),([500,500,500,500],[1.25,2.25,4.25])]
for statuses,expected in tests:
queue=iter(statuses);delays=[]
status,body=read_with_retry(lambda:(next(queue),{},{}),wait=delays.append,random_value=lambda:0.25)
assert status==statuses[-1] and delays==expected
calls=[0];delays=[]
def transport():
calls[0]+=1
if calls[0]==1:raise TimeoutError()
return 200,{},{}
assert read_with_retry(transport,wait=delays.append,random_value=lambda:0.5)[0]==200 and delays==[1.5]
print('PASS7 deterministic retry scenarios; no actual sleep/network')
if __name__=='__main__':
try:
if sys.argv[1:]==['--self-test']:self_test()
elif len(sys.argv)==1:
status,body=read_with_retry(send_public_key)
print(json.dumps(body));sys.exit(0 if 200<=status<300 and isinstance(body,dict) and body.get('success') is True else 1)
else:raise ValueError('Unknown argument')
except Exception:
print('Safe read unavailable. Check configuration/connectivity; never reuse this retry loop for uncertain mutations.',file=sys.stderr);sys.exit(2)
```
Run python 3 backoff.py against the default HTTPS API, or use an explicitly opted-in loopback fixture. The --self-test mode tests seven actual algorithm paths with injected responses/clocks and no real sleeping. It is a deterministic contract test, not seven induced service outages.
Do not copy this safe-read retry loop onto create or renew. A timeout/500 after those writes may follow a committed mutation. Read the [idempotency guide](/docs/v1/api/idempotency) first.
## Reduce calls without bypassing checks
Keep one persisted installation identity and one current token per installation. Coordinate concurrent application workers so they do not all validate at once. Respect signed next_validation_at only through your trusted verified-cache/clock policy. A new signature must still pass [product](/docs/v1/start/glossary#product)/environment/installation/status/time/[feature](/docs/v1/start/glossary#feature) checks. On explicit rejection, do not keep calling until access is granted.
## Platform quotas are different
Monthly API usage aggregates attributed requests from Sandbox and [Live](/docs/v1/start/glossary#live), including some failures after tenant context is established. It is currently counted/notified, not an enforced monthly API denial. Device/team platform allowances are displayed but are not global activation/team gates. License max_devices/max_activations are enforced separately.
New product/license/[webhook](/docs/v1/start/glossary#webhook) creation capacity is enforced for ACTIVE/TRIALING subscriptions; API license creation maps those rejections to INVALID_REQUEST (HTTP 422), not an invented PLAN_LIMIT_REACHED code. Existing [customer](/docs/v1/start/glossary#customer) validation follows its own license rules. Do not hard-code platform prices/caps from this table; configuration can change.
## Expected result
Calls respect the appropriate fixed window, wait after 429, and preserve license checks rather than turning transient failure into access.
## Common mistakes
- Assuming a 429 includes Retry-After or reports the pre-auth bucket.
- Retrying renewal automatically after a network timeout.
- Validating on every UI click rather than coordinating the verified-cache interval.
## Security notes
Rate limiting is not a complete anti-abuse guarantee. Do not bypass buckets, change identity to evade controls or reuse cached state after an explicit denial.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| 429 without delay header | Current licensing error behavior | Wait a full window with jitter |
| Shared actions throttle unexpectedly | One license-write bucket covers four actions | Coordinate management workers |
| Monthly usage exceeds display | Advisory counter is not a hard rejection | Monitor account usage and choose a suitable plan |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Idempotency and uncertain outcomes
Canonical: https://licensing.kdpika.com/docs/v1/api/idempotency
---
title: "Idempotency and uncertain outcomes"
description: "Choose stable creation keys, understand actual 24-hour replay limits, and avoid repeating uncertain license mutations."
slug: "/docs/v1/api/idempotency"
section: "API reference"
order: 39
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Deduplicate your business events durably; the current API creation key is useful but does not guarantee exactly-once fulfillment.
## What you will learn
- Choose stable keys and identical retry bodies.
- Understand expiry, concurrency and duplicate-work defects.
- Handle store fulfillment and renewal uncertainty without guessing.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Creation key contract
POST /api/v1/[licenses](/docs/v1/start/glossary#license) requires an Idempotency-Key header with 8–190 trimmed characters. The key is scoped to the whole [organization](/docs/v1/start/glossary#organization). Include a stable [product](/docs/v1/start/glossary#product)/store/event identity so unrelated operations cannot collide. Store event IDs are input to your own mapping; KDPika does not verify an external store event here.
The retained response lasts 24 hours. Reusing the unexpired key and identical decoded-body hash returns the original 201 body, including the original request_id and plaintext newly issued [license key](/docs/v1/start/glossary#license-key), plus Idempotent-Replay:true. X-Request-ID identifies the current request. A different hash returns 409 IDEMPOTENCY_CONFLICT.
The server hashes PHP JSON encoding of the decoded body. JSON whitespace is decoded away, but object member order is retained and can change the hash. Unknown fields also affect the hash even if creation ignores them. Serialize one stable request and retain its exact business meaning; do not regenerate/reorder [customer](/docs/v1/start/glossary#customer) fields between retries.
## Known limitations: not exactly once
An expired row before cleanup can be missed by the lookup and then collide with the unique insert after a new license commits. The caller receives 500 and a duplicate license can exist. Simultaneous same-key requests can both pass the lookup and commit separate licenses; one response then fails storing the idempotency record. Both issues are reproduced in isolated Sandbox tests and remain unchanged.
Do not reissue with a fresh key simply because a request timed out or returned 500. Use your durable fulfillment mapping and support to reconcile uncertain creation. No public license-list/customer-search endpoint exists to reconstruct that mapping automatically. A management GET can reconcile only when you already retained the license ID. These limitations make unattended exactly-once payment fulfillment unsupported without an approved product fix.
## Retry safety by operation
| Operation | Idempotency-Key behavior | Client decision |
| --- | --- | --- |
| GET /api/v1/public-key | Ignored / no replay | Safe bounded read retry |
| POST /api/v1/licenses | Required; retained response with known defects | Reconcile uncertain result before another write |
| POST /api/v1/licenses/activate | Ignored / no replay | Reconcile uncertain result before another write |
| POST /api/v1/licenses/validate | Ignored / no replay | Token/last-seen refresh; coordinate retries |
| POST /api/v1/licenses/deactivate | Ignored / no replay | Reconcile uncertain result before another write |
| GET /api/v1/licenses/{license} | Ignored / no replay | Safe bounded read retry |
| POST /api/v1/licenses/{license}/revoke | Ignored / no replay | Reconcile uncertain result before another write |
| POST /api/v1/licenses/{license}/suspend | Ignored / no replay | Reconcile uncertain result before another write |
| POST /api/v1/licenses/{license}/restore | Ignored / no replay | Reconcile uncertain result before another write |
| POST /api/v1/licenses/{license}/renew | Ignored / no replay | Reconcile uncertain result before another write |
| POST /api/v1/licenses/{license}/reset-devices | Ignored / no replay | Reconcile uncertain result before another write |
[Activation](/docs/v1/start/glossary#activation) rotates tokens even on an existing ACTIVE installation. Renewal extends time on every accepted call. Deactivation repeats401; repeated status/reset actions can repeat events. Database row locks serialize updates but do not identify a repeated external business event.
## Durable fulfillment example
For a verified event such as YOUR_STORE_EVENT_ID, persist a fulfillment row under a unique key of store/account/product/event. Save the normalized request, intended action and state before calling the API. Serialize workers on that row; do not use process-local locks as durable deduplication.
1. Verify the store [webhook](/docs/v1/start/glossary#webhook) or server-side purchase with your own provider integration. Expected: authenticated event, not a success-page query string.
2. Acquire the unique durable fulfillment row. Expected: only one worker owns this event; an already completed row returns its saved license mapping without another API call.
3. Use a stable creation key such as store:YOUR_STORE_ACCOUNT:YOUR_PRODUCT:YOUR_STORE_EVENT_ID. Expected: an 8–190 character key with no buyer email or card data.
4. Send the saved normalized JSON to the create operation. Expected: a documented201 with a new license ID/key, or an explicit error.
5. Persist the license mapping and secret-delivery state atomically in your system. Expected: subsequent deliveries reuse the durable record, not another API creation.
6. Mark any timeout/500 as uncertain and reconcile before releasing another write. Expected: no automatic duplicate issuance or renewal. Do not claim a provider charged the buyer unless your provider data confirms it.
A stable illustrative request key is YOUR_STORE_EVENT_ID, not a new random value on every retry. Use your provider account in the dedupe namespace when event IDs are not globally unique. This is a workflow specification, not a new provider integration or a fake payment example. Store-specific tested integration code belongs to the separate integration guides.
## Renewal requires its own deduplication
Persist each verified renewal event and expected old/new expiration before calling renew. Renew has no replay key. If a response is lost, compare a known license summary and your durable record; do not add the same days automatically a second time. Concurrent unrelated renewals need deliberate business serialization, because merely reading then writing is not compare-and-swap.
## Retention and secrecy
The server currently stores the complete creation response, including plaintext license_key, in its idempotency record for 24 hours; cleanup depends on a functioning scheduler. Treat database access/backups as sensitive. Your own customer-key [delivery](/docs/v1/start/glossary#delivery) must be authorized and confidential. The API does not automatically email the key to the buyer.
## Expected result
A replayed business event does not trigger an intentional second write from your worker, and uncertain outcomes are surfaced for reconciliation.
## Common mistakes
- Choosing a new random header for every attempt of the same event.
- Assuming renew supports the creation replay mechanism.
- Advertising exactly-once fulfillment despite the current expiry/concurrency defects.
## Security notes
Verify purchases on your backend. Never put provider secrets or [developer API keys](/docs/v1/start/glossary#developer-api-key) in buyer applications. Keep replay bodies and fulfillment records private.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| 409 IDEMPOTENCY_CONFLICT | Different decoded request hash/member order | Use the saved original request; do not hide the conflict with a new key |
| 500 after create | Uncertain work, including documented replay defects | Reconcile; retain current/original [request IDs](/docs/v1/start/glossary#request-id) |
| Expiry extended twice | Same renewal event repeated without durable dedupe | Investigate the event mapping; do not automate further extensions |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# OpenAPI specification
Canonical: https://licensing.kdpika.com/docs/v1/api/openapi
---
title: "OpenAPI specification"
description: "Download the complete source-validated OpenAPI 3.1 licensing contract, including actual statuses, errors, headers and examples."
slug: "/docs/v1/api/openapi"
section: "API reference"
order: 40
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Use the machine-readable reference alongside the endpoint guides; schema validation is not an access-control decision.
## What you will learn
- Download identical JSON/YAML contracts.
- Understand recommended input schemas, response strictness and signing limits.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Downloads
- [OpenAPI JSON](/docs/openapi.json)
- [OpenAPI YAML](/docs/openapi.yaml)
Both files describe the same API v1 contract using OpenAPI 3.1.1 and JSON Schema2020-12. They include all 11 registered licensing operations,25 named request/response components, [developer](/docs/v1/start/glossary#developer)/installation/public-app credential schemes, required management [scopes](/docs/v1/start/glossary#scope) and the exact current error/header behavior. They do not invent [customer](/docs/v1/start/glossary#customer)/[product](/docs/v1/start/glossary#product)/list/payment endpoints.
The only server URL is https://licensing.kdpika.com/api/v1. Sandbox/[Live](/docs/v1/start/glossary#live) is bound by credentials and the [activation](/docs/v1/start/glossary#activation) header, not a separate host. x-required-scope records a developer scope without pretending the bearer credential is OAuth.
## Inputs, responses and examples
Input schemas describe supported recommended JSON types. The actual PHP backend also trims/casts some values and ignores unknown fields; a strict schema cannot describe those malformed-input coercions as if every violation were rejected identically. Use the endpoint constraints/conventions for that distinction.
Response schemas describe every emitted nested field and nullability. Example success bodies are captured from the isolated Sandbox; [license keys](/docs/v1/start/glossary#license-key)/tokens are replaced with obvious placeholders. The 69 per-operation error examples use actual captured errors. A shared global INTERNAL_ERROR example is explicitly identified when reused across operations instead of claiming a separate induced outage for each handler.
Dates use UTC-labelled ISO8601 offsets as emitted by the builders, with the known database-session display-time limitation documented. [Features](/docs/v1/start/glossary#feature) can be a dynamic object or an empty array in current PHP output. Do not assume every feature is boolean.
A JSON-schema match does not verify Ed25519, preserve signed numeric tokens or enforce product/environment/installation/time policy. Read [signed responses](/docs/v1/security/signed-responses) before authorizing access.
## Validation and drift checks
The authoring tool checks exact source hashes and route inventory before generation. Real local Sandbox requests validate success/error bodies against the generated schema. JSON and YAML equality, every example, all schema definitions and negative fixtures are checked. Standard Spectral OAS rules produce zero errors/warnings/hints; a deliberately missing operationId fails the gate.
The local Spectral JS API uses pinned packages and no disabled rule set. No Node runtime or external schema fetch is required on hosting; the published files are compiled artifacts. A new product behavior must update source facts, schemas, samples and executed tests before publication.
## Importing the contract
1. Download JSON or YAML. Expected: the same11 operations, not a login page.
2. Import that file into your OpenAPI 3.1-compatible local editor/client. Expected: request/response schemas and three credential schemes are recognized.
3. Configure only your Sandbox test credentials. Expected: no Live operation or real customer mutation during exploration.
4. Compare generated clients against the actual runnable examples and vectors. Expected: signature bytes, null handling and error fallbacks are tested rather than assumed.
## Expected result
You have the actual v1 contract and can validate response shape without mistaking it for cryptographic or business authorization.
## Common mistakes
- Using an OpenAPI 3.0-only validator for 3.1 type/null/schema features.
- Treating request schema guidance as proof of strict PHP input rejection.
- Replacing Ed25519 verification with a schema or success-flag check.
## Security notes
Keep generated client credentials private, use HTTPS and never auto-upload populated test environments or private captures to a public schema editor.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Importer rejects type arrays/const | Tool supports an older OpenAPI/JSON Schema dialect | Use a3.1-compatible importer |
| Documented route missing from generator | Stale exported contract or unsupported version | Download the current exact file and compare the operation |
| Valid JSON but signature failure | Shape is not byte authenticity | Run the published verifier/vectors and fail closed |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Contract testing
Canonical: https://licensing.kdpika.com/docs/v1/api/contract-testing
---
title: "Contract testing"
description: "Test every licensing operation and error shape against OpenAPI using a safe isolated Sandbox fixture and reproducible checks."
slug: "/docs/v1/api/contract-testing"
section: "API reference"
order: 41
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Run destructive lifecycle checks only in an isolated [Sandbox](/docs/v1/start/glossary#sandbox) and preserve evidence without credential-bearing response dumps.
## What you will learn
- Understand the executed contract coverage and its limits.
- Reproduce safe contract validation in your own development environment.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Current executed coverage
The project test suite sends105 real HTTP requests through the unchanged front controller, Router, controller, services, Signer and local MySQL. It validates all 11 successful operation shapes and all 22 licensing/router error identifiers with 954 assertions. It checks [request IDs](/docs/v1/start/glossary#request-id), JSON content types, stateless behavior, missing permissive CORS, rate headers and error-header omissions.
Eight real limiter buckets are primed to the boundary using fresh synthetic test IPs/keys through the actual RateLimiter service; the next HTTP request must fail429. No limiter history is cleared and no public hosting load test is run.
Actual MySQL row-lock races exercise missing-row errors after controller preflight. A second local PHP request process reproduces simultaneous same-key creation: two [licenses](/docs/v1/start/glossary#license) can commit before one response fails500. An expired idempotency row also reproduces a duplicate-before 500. These are disclosed [product](/docs/v1/start/glossary#product) findings, not fixes.
An intentionally missing public-key path in a loopback-only test router exercises the actual global500 boundary. No real signing key is deleted and no hosted outage is induced. Scheduled cron [delivery](/docs/v1/start/glossary#delivery) is suppressed only by the local harness, so audit/API metadata/queued events are real but no external [webhook](/docs/v1/start/glossary#webhook)/email/payment is sent.
## Sample and schema checks
Five core transport programs each execute all 11 operations and four refusal paths. Ten additional REST languages each execute public-key/activate/validate/deactivate plus wrong-email and post-deactivation refusals, with real local redirect/untrusted-TLS guards. Native Windows/Linux execution is not claimed by macOS tests.
Four SDK error-UX programs execute50 synthetic policy inputs using actual result models, plus actual NOT_ACTIVATED checks from fresh SDK storage. This tests handling, not runtime emission of every unrelated installer/billing/operational error. Those namespaces are clearly separated in [Errors](/docs/v1/api/errors).
OpenAPI JSON/YAML equality, all examples, source-hash/route coverage and25 schema definitions are checked. Negative response fixtures and an intentionally missing operationId prove that validators fail rather than pass every input.
## Running the repository tests
The repository contains tests/docs-p6-contract.py, tests/docs-p6-spec.py, tests/docs-p6-samples.py, tests/docs-p6-rest-languages.py, tests/docs-p6-errors.py and tests/docs-p6-collections.py. These are local development tools, not scripts to upload or run against existing [customer](/docs/v1/start/glossary#customer) data. The exact guarded fixture commands and pinned dependencies are recorded in docs-program/tooling/p6/README.md in the project source.
1. Create a new explicitly named local test database with the guarded fixture setup. Expected: setup refuses an existing database and does not print credentials.
2. Start the loopback-only PHP test router against that fixture. Expected: no production URL or [Live](/docs/v1/start/glossary#live) database is used.
3. Build the generated OpenAPI and exact source examples with pinned local toolchains. Expected: compilation/lint failures stop the run.
4. Run the suites against the same explicit local Sandbox fixture. Expected: sanitized assertion/status evidence, not raw token/key output.
5. Review known defects separately from schema drift. Expected: no silent licensing/payment/SDK logic change in a documentation patch.
If you only consume the API, use the [collections](/docs/v1/api/collections) in your own Sandbox instead of reproducing server fault/locking tests.
## Expected result
Contract drift and refusal regressions are detected, while test-only faults and actual production behavior remain clearly distinguished.
## Common mistakes
- Running a mutation suite against a real customer workspace or Live key.
- Claiming mocked fault/UX cases are real provider outages or every OS execution.
- Fixing a product defect silently to make a documentation test green.
## Security notes
Never upload test routers, fixture keys or private reports. Keep new local fixtures separate from prior data; do not reset the project database.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Guard refuses a database | Not an explicitly isolated P6 fixture | Use a new local guarded fixture, not production |
| Schema/example failure | Contract drift or malformed sample | Inspect the sanitized path/code and source builder |
| Creation500 with extra local row | Known idempotency defect reproduced | Record the finding; do not hide it with retries or a new key |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# API client collections
Canonical: https://licensing.kdpika.com/docs/v1/api/collections
---
title: "API client collections"
description: "Download Postman, Insomnia and Bruno collections with Sandbox variables, assertions and a complete11-operation test sequence."
slug: "/docs/v1/api/collections"
section: "API reference"
order: 42
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Run a real serial [Sandbox](/docs/v1/start/glossary#sandbox) lifecycle using your preferred API client; the sequence ends by revoking its test [license](/docs/v1/start/glossary#license).
## What you will learn
- Import all three native formats and configure test-only variables.
- Understand mutation order, local variables and credential precautions.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Downloads
- [Postman 2.1 collection](/docs/downloads/postman.json)
- [Postman Sandbox environment with placeholders](/docs/downloads/sandbox.postman_environment.json)
- [Insomnia legacy-v4 JSON export](/docs/downloads/insomnia.json)
- [Bruno collection ZIP](/docs/downloads/bruno.zip)
Every collection is generated from the same11-operation route/auth/body model as the OpenAPI and endpoint pages. All exported credential values are obvious placeholders. The exports are files on KDPika hosting; no external API-client cloud service is needed by the platform itself.
## Important: this sequence changes test data
Run serially in Sandbox: public key, create, activate, validate, get, deactivate, suspend, restore, renew, reset-devices and revoke. It creates a test [customer](/docs/v1/start/glossary#customer)/license, consumes an [activation](/docs/v1/start/glossary#activation), extends expiry by 30 days and finally revokes that license. It does not make a payment or change a [developer](/docs/v1/start/glossary#developer) subscription.
Do not run the full sequence on an existing customer license or in parallel. Do not automatically repeat an uncertain create/renew after a timeout/500. Use a new business-event identity only for an intentionally new test lifecycle, not to conceal an uncertain previous result.
The collections set license_id/license_key after creation and activation_token after a successful Sandbox activation in temporary local/run variables. Read your client version documentation on variable lifetime, histories, cloud sync and reporters. Client applications can persist requests/responses independently of these scripts; this is different from the in-memory website console. Never export populated secrets or share raw response histories.
## Configure variables
| Variable | Set it to |
| --- | --- |
| base_url | https://licensing.kdpika.com/api/v1 |
| app_id | Your Sandbox [product](/docs/v1/start/glossary#product) [public App ID](/docs/v1/start/glossary#public-app-id) |
| api_key | Sandbox developer key with licenses:read,licenses:write,[devices](/docs/v1/start/glossary#device):write |
| product_id | Internal product ID or its scoped slug |
| plan_id | Active product-plan ID or scoped slug |
| customer_email | A synthetic test address, not a real buyer |
| installation_id | One stable random test [installation ID](/docs/v1/start/glossary#installation-id),16–255 characters |
| idempotency_key | One unique intentional test-event ID,8–190 characters |
| license_id/license_key/activation_token | Runtime results saved by the sequence; start with placeholders |
| allow_loopback | Leave 0 for hosting;1 only for an explicit local fixture |
Guards require an HTTPS /api/v1 base, or an explicitly opted-in localhost/127.0.0.1:port test base. Management requests require a Sandbox developer-key prefix; activation always sends X-Environment:SANDBOX and requires a TEST-prefixed license. Do not replace guards to force a [Live](/docs/v1/start/glossary#live) run. Redirects are disabled in the provided requests; certificate verification remains enabled.
## Postman
1. Import the collection JSON and placeholder environment. Expected:11 requests in the documented order.
2. Set private/local Sandbox variable values, without publishing or syncing credentials. Expected: the create request can authenticate with only the required [scopes](/docs/v1/start/glossary#scope).
3. Run the collection once serially. Expected:35 successful assertions, including 201 for create, success/[request IDs](/docs/v1/start/glossary#request-id) and Sandbox validation fields.
The v2.1 export is tested with Newman 6.2.3 as a local compatibility engine. Newman is now unmaintained and is not recommended as a new production dependency. Postman v12 Native Git uses the newer v3 format; this download is the explicit2.1 JSON import format, not a v3 Native Git project. Use a maintained Postman client/CLI for your ongoing workflow and check its supported import format.
[Postman format/runtime guidance](https://learning.postman.com/docs/reference/newman-cli/command-line-integration-with-newman/) explains that distinction.
## Insomnia
1. Import the legacy-v4 JSON export using From File. Expected: a Sandbox workspace/environment and11 ordered requests; modern Insomnia retains v4 import compatibility.
2. Set the private Sandbox environment values. Expected: no Live key is used.
3. Run the collection serially. Expected:35 response tests, including correct201 handling and generated temporary license/token variables.
The actual import/execution is tested with checksum-verified Inso CLI 13.3.1, not claimed as a desktop GUI import test. The CLI run uses the exported file, disables prompts and retains only sanitized status/assertion evidence. [Insomnia import formats](https://developer.konghq.com/insomnia/import-export/) and [collection runner options](https://developer.konghq.com/inso-cli/reference/run_collection/) document the supported file workflow.
This export targets the Liquid template engine in Insomnia13.3.1. Its URL-render guard is independent of pre-request scripts: a refused key/base renders a non-network kdpika-blocked URI. The tested CLI import did not apply the pre-request script, so relying only on a thrown script error or skip flag would not be safe. The actual native negative tests prove that the delivered guard sends no HTTP request for Live keys/licenses or unapproved HTTP. Use the tested client version; older template engines are not claimed compatible. [Pinned Insomnia template engine source](https://github.com/Kong/insomnia/blob/core%4013.3.1/packages/insomnia/src/templating/index.ts) identifies Liquid.
## Bruno
1. Extract the ZIP into a local directory and open that collection. Expected: bruno.json, Sandbox environment and11 .bru requests.
2. Put credentials in your private local secret/environment setup, not a committed shared file. Expected: placeholder exports remain secret-free.
3. Select Sandbox and run serially with the safe scripting sandbox. Expected: all 11 real requests and35 tests pass.
The export is executed with Bruno CLI 4.2.1 using its safe sandbox. Runtime variables use bru.setVar rather than persisting generated tokens into the shared environment export. [Bruno scripting API](https://github.com/usebruno/bruno-docs/blob/main/testing/script/javascript-reference.mdx) describes its environment/runtime distinction.
## What the assertions do not prove
The collection assertions check HTTP status, success/request IDs and selected Sandbox fields. Full nested schema checks run in the [contract tests](/docs/v1/api/contract-testing). An assertion pass is not Ed25519 verification, offline licensing, exactly-once fulfillment or a real payment test.
## Expected result
Your chosen client imports the actual native files and completes a serial test lifecycle without a Live key or real payment.
## Common mistakes
- Running renew/revoke repeatedly against a license you want to keep.
- Confusing temporary script variables with a guarantee that the API client never saves history.
- Disabling TLS/redirect guards to get past a configuration problem.
## Security notes
Never share populated environments, raw collection reports or response histories. Use a temporary test workspace and synthetic customer details. Keep all device credentials private.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Pre-request guard rejects key/base | Wrong scope/environment, placeholder or URL | Set the actual Sandbox values and exact /api/v1 base |
| Later requests use placeholders | Collection was not run serially from creation | Start a deliberate fresh lifecycle; inspect safe status tests |
| 201 counted as failure by a client option | Option expects only200 | Use the supplied per-operation assertions; create legitimately returns 201 |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Try the Sandbox API
Canonical: https://licensing.kdpika.com/docs/v1/api/try-it
---
title: "Try the Sandbox API"
description: "Inspect real same-origin Sandbox licensing requests without storing credentials, using Live keys or requiring a developer account to read."
slug: "/docs/v1/api/try-it"
section: "API reference"
order: 43
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Use a test [license](/docs/v1/start/glossary#license) to inspect [activation](/docs/v1/start/glossary#activation), validation and deactivation; this console never grants application access.
## What you will learn
- Send real [Sandbox](/docs/v1/start/glossary#sandbox) requests and see the API reply.
- Understand in-memory credentials, [device](/docs/v1/start/glossary#device) seats and the [Live](/docs/v1/start/glossary#live) guard.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Before sending a request
Create a Sandbox [product](/docs/v1/start/glossary#product)/plan/license using the [quickstart](/docs/v1/start/quickstart). Copy its [public App ID](/docs/v1/start/glossary#public-app-id) and TEST-prefixed [license key](/docs/v1/start/glossary#license-key). Reading documentation requires no account; obtaining your own test license requires your Sandbox workspace. Do not use a real buyer email or any Live credential.
The console uses the current site origin and a fixed SANDBOX activation header. There is no user-supplied target URL or Live environment selector. The TEST-prefix input check is helpful UX; the actual API environment-scoped lookup is the trust boundary. [Developer API keys](/docs/v1/start/glossary#developer-api-key) are not accepted here.
## Interactive inspector
The same-origin Sandbox inspector is available on this page in a browser with JavaScript. It sends test activation/validation/deactivation requests, holds the returned token only in memory and never accepts a pasted activation token. The source examples remain readable without JavaScript.
## How to use it
1. Retrieve the public key if you want to inspect the current verification-key response. Expected: JSON with algorithm/key_id/public_key/encoding.
2. Enter your test App ID, Sandbox key and optional matching email. Expected: a temporary random [installation ID](/docs/v1/start/glossary#installation-id) appears only on this page.
3. Confirm that the data is for testing, then activate. Expected: the real API accepts or rejects the request; a successful Sandbox token is held only in this page memory.
4. Validate that same installation. Expected: a fresh signed snapshot or an explicit refusal; no application [feature](/docs/v1/start/glossary#feature) is enabled by this inspector.
5. Deactivate before clearing/reloading if you want to release the seat. Expected: success releases the concurrent seat, but does not reset lifetime activation_count.
6. Clear sensitive data when finished. Expected: form values/token/response are removed from this page; clearing by itself does not deactivate a seat.
Refreshing, navigating away or clearing loses the console token/temporary identity. A new activation can consume another lifetime activation. Recover an abandoned test seat through your supported Sandbox device controls; do not mistake cache clearing for deactivation.
## Privacy and security boundaries
Requests go directly to this site API over HTTPS, never through a documentation proxy or external service. Authorization is sent only for the in-memory token obtained by this console successful Sandbox activation. You cannot paste a Live [activation token](/docs/v1/start/glossary#activation-token). Validation/deactivation remain disabled until that Sandbox activation succeeds.
Credentials are not written to localStorage, sessionStorage, cookies, indexedDB, feedback, analytics or console logs by this inspector. The form and response exist temporarily in browser memory. HTTP requests necessarily transmit your test inputs to the API. The normal API metadata logger records method/path/status/[request ID](/docs/v1/start/glossary#request-id) and hashed IP, not Authorization or request bodies.
The response token is hidden by default; a deliberate reveal checkbox shows it only on this screen. A signed license payload may include your supplied test email and other test [customer](/docs/v1/start/glossary#customer) fields. Avoid screenshots or screen sharing with sensitive details. Your own browser extensions, [developer](/docs/v1/start/glossary#developer) tools and device administrator are outside the page security boundary.
The console deliberately inspects transport and server behavior; it does not implement local Ed25519 access authorization or fix the SDK/idempotency limitations. Use the [verification vectors](/docs/v1/security/test-vectors) when building an application client.
## Expected result
You can see real Sandbox HTTP status/body/request ID while token operations remain bound to a Sandbox activation created in this page.
## Common mistakes
- Using a Live license or trying to paste a developer API key.
- Clearing/reloading before deactivation and assuming the seat was released.
- Treating a displayed JSON success as your application entitlement gate.
## Security notes
Use only test data. No hosted request is automatically sent when opening this page. Unknown/invalid responses do not enable token actions. JavaScript is required only for sending requests, not for reading examples.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Validate/deactivate disabled | No successful Sandbox activation in this page memory | Activate a test installation here first |
| Key rejected before request | Not a TEST-prefixed Sandbox key | Use a license from your Sandbox workspace |
| Email mismatch | Nonempty email does not match customer | Use the license customer email or intentionally test key-only mode |
| Request interrupted | Network/timeout/redirect/unexpected response | No access is granted; reconcile any consumed test seat before retrying |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# API v1 changelog
Canonical: https://licensing.kdpika.com/docs/v1/api/api-changelog
---
title: "API v1 changelog"
description: "Track source-verifiable API documentation snapshots separately from product releases and SDK versions."
slug: "/docs/v1/api/api-changelog"
section: "API reference"
order: 44
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
This changelog records verified contract snapshots; documentation publication is not a new payment or licensing release.
## What you will learn
- Distinguish API contract documentation from SDK/[product](/docs/v1/start/glossary#product) release versions.
- Find verified changes without invented historic dates.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## 2026-10-17: complete v1 reference snapshot
The current source registers11 licensing operations: public-key retrieval, creation, [activation](/docs/v1/start/glossary#activation), validation, deactivation, scoped retrieval, revoke, suspend, restore, renew and reset-devices. The reference records25 response/request components, exact credentials/[scopes](/docs/v1/start/glossary#scope),69 per-operation error examples and the documented current headers. This entry describes the P6 authoring snapshot, not an API behavior change.
The local contract suite captures all 11 successes and22 API/router error identifiers. Two creation-idempotency defects remain disclosed rather than silently corrected. SDK versions are separate from API v1; the currently audited official packages identify version 1.0.0.
No earlier API release date is reconstructed from undocumented assumptions. An older file timestamp is not evidence that a behavior was deployed on that date.
## Change entry format
| Field | What to record |
| --- | --- |
| Date | Actual verified publication/deployment date in UTC |
| Affected contract | Version, operation, fields, statuses, headers or error codes |
| Compatibility | Additive, documentation correction or breaking; explain why |
| Migration | Required client change, test vectors and upgrade guide |
| Evidence | Source commit and tested Sandbox response without secrets |
| Announcement | Actual notification channel/notice dates, not a presumed SLA |
A future entry must distinguish documentation correction from a changed server contract. Do not list a proposed [feature](/docs/v1/start/glossary#feature) as an available operation.
## Known limitations
The current API has no historical signing-key ring, no list/[customer](/docs/v1/start/glossary#customer)/product CRUD surface and no universal mutation replay. Existing canonicalization/cache and idempotency issues remain visible in the security/reference guides. This page does not close those product findings.
## Expected result
You can identify the documented v1 snapshot without confusing it with an SDK upgrade or an unverified production release.
## Common mistakes
- Treating this reference date as the original API launch date.
- Calling a documentation correction a payment/security product fix.
## Security notes
Changelog examples and evidence must exclude secrets, customer data and internal URLs. A change entry is not a compliance or uptime claim.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Client behavior differs | Different package/server revision or documented limitation | Compare operation/schema and preserve [request ID](/docs/v1/start/glossary#request-id) |
| No historic entry | No verifiable release evidence | Ask support rather than infer a date |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# Versioning and deprecation
Canonical: https://licensing.kdpika.com/docs/v1/api/versioning-and-deprecation
---
title: "Versioning and deprecation"
description: "Understand API v1 compatibility boundaries, current notification limitations and a practical upgrade checklist."
slug: "/docs/v1/api/versioning-and-deprecation"
section: "API reference"
order: 45
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/start/authentication", "/docs/v1/api/conventions"]
draft: false
---
Use the versioned API contract and test upgrades; no numeric deprecation-notice guarantee is currently established.
## What you will learn
- Identify breaking versus additive contract changes.
- Separate actual policy from future commitments.
- Use a complete upgrade-guide template.
## Prerequisites
- Read [API conventions](/docs/v1/api/conventions).
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
## Current v1 boundary
The URI prefix /api/v1 identifies the current licensing contract. There is no registered /api/v2 contract. SDK 1.0.0 is a package version, not a second API version. There is no date-based API-version negotiation header. Unknown/wrong-version routes return the documented router error.
The intended compatibility practice is to preserve documented v1 request/response meaning within v1. This is a documentation/maintenance policy, not evidence of a contractual support term or SLA. The current code and signed-byte vectors are the auditable behavior. Known bugs are documented, not recast as desirable guarantees.
## Breaking and additive changes
| Change | Compatibility consideration |
| --- | --- |
| Remove/rename an operation or required field | Breaking |
| Add a required request header/credential/[scope](/docs/v1/start/glossary#scope) | Breaking |
| Change a field type/nullability/enum meaning or status semantics | Potentially breaking; test strict clients and access policy |
| Change canonical bytes, signature algorithm or trusted-key distribution | Security-critical breaking/migration concern |
| Add an optional field or new operation | Usually additive, but strict schema consumers must deliberately handle unknown fields |
| Add an error code | Clients must fail closed on unknown codes and show a safe fallback |
| Correct documentation to match unchanged source | Documentation correction, not necessarily a new API release |
Response schemas deliberately show every current field. If you generate clients with strict unknown-field rejection, review additive changes instead of assuming they are invisible. Do not accept an unsigned or invalid response merely to tolerate a changed schema.
## Notice periods and announcements
No minimum numeric notice period, deprecation email pipeline, Sunset/Deprecation header or published v1 end-of-support date is implemented/confirmed. Do not invent30/90/180-day promises. API responses do not currently include those deprecation headers. Refer to the actual changelog and contact hi@kdpika.com for an announced migration; absence of a header is not an unlimited-lifetime guarantee.
Before a breaking release is offered, its owner should approve the supported versions, advance notice dates, migration guide, [delivery](/docs/v1/start/glossary#delivery) channels and retirement policy. The approved notice belongs in the changelog and account communication. This is a maintenance recommendation, not a claim that notifications have already been sent.
## Upgrade-guide template
| Section | Required concrete content |
| --- | --- |
| Target/source versions | Exact API/SDK revisions and supported runtimes |
| Affected integrations | Named operations, fields, headers, errors, signatures and storage paths |
| Before and after | Full runnable requests/responses, not code fragments |
| Data and credentials | Whether keys/tokens/cache formats change, safe handling and rollback |
| Test procedure | Sandbox positive/negative/vector/expiry/offline cases with expected outcomes |
| Rollout | Pinning, staged deployment, monitoring and support escalation |
| Retirement | Owner-approved notice, actual dates/channels and supported overlap |
| Verification | Source evidence, executed samples and remaining limitations |
1. Pin your current SDK/client version and keep a tested rollback package. Expected: the existing installation path remains recoverable.
2. Compare the exact operation schemas, error handling and signature vectors. Expected: each changed assumption is identified.
3. Run the new client through Sandbox [activation](/docs/v1/start/glossary#activation)/validation/refusal/deactivation and offline/clock tests as applicable. Expected: no [feature](/docs/v1/start/glossary#feature) grant after a failed verification.
4. Update secure storage and credentials only according to the actual migration. Expected: no accidental token sharing or broad key exposure.
5. Roll out gradually and watch safe request-ID/error metadata. Expected: failures are visible without payload/credential logging.
6. Retire the old client only after your verified rollout criteria and any actual notice period are satisfied. Expected: no invented overlap guarantee.
## Expected result
Upgrade decisions are based on an explicit contract diff and executed Sandbox checks, with current policy limitations visible.
## Common mistakes
- Using an SDK version as the API path or inventing a Stripe-style date header.
- Assuming a fixed deprecation notice/Sunset header exists because other APIs use one.
## Security notes
Test signed-byte compatibility and semantic refusal paths before rollout. Never recover from a version mismatch by bypassing signature, TLS or installation checks.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Unknown route after upgrade | Unsupported API prefix/path | Use documented v1 until an actual new version is offered |
| No Sunset header | Not implemented in current API | Read the actual announced migration and ask support |
| Signature mismatch | Changed client serialization/trust or known canonicalization limitation | Fail closed and run the exact published vectors |
## Next steps
Review [the endpoint reference](/docs/v1/api/public-key) and [signature verification](/docs/v1/security/verify-ed25519).
## Related
- [Authentication](/docs/v1/start/authentication)
- [API conventions](/docs/v1/api/conventions)
- [License lifecycle](/docs/v1/start/license-lifecycle)
# SDK overview and downloads
Canonical: https://licensing.kdpika.com/docs/v1/sdks/overview
---
title: "SDK overview and downloads"
description: "Choose an official SDK or a REST client, verify source-package checksums, and understand actual runtime and security boundaries."
slug: "/docs/v1/sdks/overview"
section: "SDKs and languages"
order: 46
audience: "all"
level: "intermediate"
last_updated: "2026-10-21"
version: "v1"
related: ["/docs/v1/start/quickstart", "/docs/v1/security/offline-mode"]
draft: false
---
Choose a client by runtime and trust model, then test the exact package before distributing your integration.
## All languages
[Official SDK](/docs/v1/start/glossary#official-sdk) source packages handle the client lifecycle and signed decisions, subject to the documented compatibility limits. REST projects are lower-level integration examples, not additional official SDKs.
### Official SDK source packages
- [Python](/docs/v1/sdks/python)
- [JavaScript / Node.js](/docs/v1/sdks/javascript)
- [PHP](/docs/v1/sdks/php)
- [C# / .NET](/docs/v1/sdks/csharp)
### REST client projects
- [TypeScript](/docs/v1/sdks/typescript)
- [Java](/docs/v1/sdks/java)
- [Kotlin](/docs/v1/sdks/kotlin)
- [Swift](/docs/v1/sdks/swift)
- [Dart](/docs/v1/sdks/dart)
- [Go](/docs/v1/sdks/go)
- [Rust](/docs/v1/sdks/rust)
- [Ruby](/docs/v1/sdks/ruby)
- [C](/docs/v1/sdks/c)
- [C++](/docs/v1/sdks/cpp)
[My language is not listed: build a client](/docs/v1/sdks/build-your-own-client). Your client must verify signatures and semantic bindings before allowing access.
## What you will learn
- Distinguish four [official SDKs](/docs/v1/start/glossary#official-sdk) from native REST examples.
- Verify package source/checksums and runtime requirements.
- Understand version, platform, storage and known release limitations.
## Prerequisites
- Complete [the quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [signature verification](/docs/v1/security/verify-ed25519).
## Choose an integration
Use an official SDK when its runtime matches your application and its documented release limitations are acceptable. These four packages handle [customer](/docs/v1/start/glossary#customer) installation licensing, not [developer](/docs/v1/start/glossary#developer) management or your software-buyer payments. Keep lmp_ developer keys on your trusted backend. REST programs are not additional official SDK packages.
| Language | Package identity | Version label | Declared minimum | Actual requirement / boundary | Application model |
| --- | --- | --- | --- | --- | --- |
| python | kdpika-licensing | 1.1.5 | >=3.11,<3.15 | Python 3.11–3.14 | PyPI package; import kdpika_licensing |
| php | kdpika/license-sdk | 1.0.0 | ^8.1 | PHP8.2+ (readonly result class) | Composer PSR-4 backend library |
| javascript | @kdpika/license-sdk | 1.0.0 | >=18 | Node18+; test each deployed runtime | module; Node/Electron main process only |
| csharp | KDPika.LicenseSdk | 1.0.0 | net7.0 | .NET7+ compatible target; prefer maintained runtime | C# project reference; async client |
Python 1.1.5 is published on [PyPI](https://pypi.org/project/kdpika-licensing/) with MIT-licensed [GitHub source](https://github.com/kdpika/kdpika-licensing). Install with python -m pip install "kdpika-licensing==1.1.5". PHP, JavaScript and C# remain direct source distributions; no npm, Packagist or NuGet publication is claimed.
The PHP version label comes from the existing download filename; composer.json has no version field. Its ^8.1 declaration conflicts with the readonly result class, which needs PHP 8.2+. C# targets net7.0; this is not a promise that .NET 7 remains vendor-supported. Choose a maintained deployment runtime and test compatibility.
## Download and verify
| Language | Source ZIP | Bytes | SHA-256 |
| --- | --- | --- | --- |
| python | [kdpika-python-sdk-1.1.5.zip](/docs/downloads/kdpika-python-sdk-1.1.5.zip) | 151872 | f8389b804510b7a38ba299793a0a2aba2d55eba147d37a682cd94b39d08a5a05 |
| php | [kdpika-php-sdk-1.0.0.zip](/docs/downloads/kdpika-php-sdk-1.0.0.zip) | 16532 | fd6a1ca7c2111fcbbe090f0065e1ee9bc70a439904bf1b049234e49c7ba2f108 |
| javascript | [kdpika-javascript-sdk-1.0.0.zip](/docs/downloads/kdpika-javascript-sdk-1.0.0.zip) | 14854 | d0f3fd6c10e3264eebe37bfa5bb1e6ef7ddabe3a3acb37fc3ffd9a0d5b518d30 |
| csharp | [kdpika-csharp-sdk-1.0.0.zip](/docs/downloads/kdpika-csharp-sdk-1.0.0.zip) | 16422 | cc9a591ab5b3a23ed7b172c5e67cbc92dad0f0c773d853e00c2e6da3c588d9d7 |
Each ZIP has one kdpika-LANGUAGE-sdk top-level directory and the exact package source, tests and documentation. Generated obj/bin, node_modules, vendor, caches, private credentials and machine files are excluded. The hashes cover the downloadable ZIP bytes, not an unrelated package-manager registry artifact.
1. Download the ZIP for your language from this page. Expected: the versioned filename in the table.
2. Calculate SHA-256 before extracting. Expected: the full digest exactly matches the table; otherwise stop and download again from the official site.
3. Extract into a clean project directory. Expected: a single SDK directory, with its package metadata and src/project files.
4. Follow the relevant language guide and pin your application dependencies. Expected: your build uses this exact source, not an unverified registry package.
On macOS use shasum -a 256 with the downloaded filename. On Linux use sha256sum. On Windows use Get-FileHash -Algorithm SHA256. A matching checksum detects accidental drift against this HTTPS page; it is not an independent publisher signature or a [license](/docs/v1/start/glossary#license) grant.
## Platform and trust boundaries
The source is intended for Windows, macOS and Linux where the selected runtime, TLS trust store and dependencies are supported. Python 1.1.5 core checks passed across Python 3.11–3.14 on Windows, Linux and Intel macOS; a fresh PyPI install also passed on Apple Silicon. The other SDKs retain their recorded source-test boundaries. GUI frameworks have their own platform limits.
Python can prefer an available OS keyring. PHP, Node and C# use permission-protected files, not automatic Keychain/DPAPI/Secret Service protection. See the tested [storage adapters](/docs/v1/security/secure-token-storage). No SDK is safe to expose as unrestricted browser management code. Electron uses its main process and a narrow authenticated IPC boundary.
Installation identity is random and persistent. [Activation](/docs/v1/start/glossary#activation) also sends a descriptive [device](/docs/v1/start/glossary#device) name/platform/app version from the runtime; that can include a machine hostname. Do not claim no device metadata is collected. Neither an installed package nor its stored token proves the current license is valid.
## Release limitations to test
JavaScript loses PHP integral-float spellings such as 19.0 during JSON serialization. C# cache serialization can change Unicode encoding. Those earlier Python 1.0.0 canonicalization defects were corrected in Python 1.1.5; use the current package, not the retained legacy archive. These can reject a real valid server signature. They are not permission to bypass Ed25519, strip signed fields or re-sign on the client.
All [feature](/docs/v1/start/glossary#feature) helpers must be used only after a valid result. A failed result can still carry signed features; validity/status/time/environment/installation rules remain essential. A successful deactivation has valid=true for the release operation but does not authorize a protected feature.
No historical signing-key ring or automatic overlap is supplied. Pinned-key updates require a controlled application rollout. Offline cache acceptance is bounded by signed grace/expiry and clock checks, not unlimited offline access.
## Package licensing and version policy
Python 1.1.5 is MIT-licensed. PHP/JavaScript metadata remains proprietary; C# has no package license field. Non-Python source ZIPs retain their LICENSE information notices and observed-version CHANGELOG files. The Python archive comes from the exact verified PyPI source release. Those notices do not invent an open-source or redistribution grant. Contact hi@kdpika.com for the applicable permission/terms before redistribution. The legacy PHP repository test retains its original autoload assumption; tests/standalone.php is the new package-local entry with the same assertions and no SDK behavior change.
The current Python release is 1.1.5; the other source SDK labels remain 1.0.0. Do not infer older releases, a support-duration SLA or an SDK compatibility promise from a filename. Application maintainers should pin the version/hash, compare the API surface and execute the signed-byte/cache/offline/refusal tests before upgrades. API v1 and SDK package versions are independent.
## 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
- Assuming a named registry release exists because a package name is shown.
- Treating PHP 8.1 or every native OS as tested.
- Enabling features from result payloads without a validity check.
## Security notes
Never ship a [developer API key](/docs/v1/start/glossary#developer-api-key) or disable signature/TLS verification. ZIP checksums are integrity metadata, not an open-source grant or proof that a local app cannot be patched.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Checksum differs | Wrong version, incomplete download or changed source | Stop; compare the exact current filename/digest |
| PHP parse error on 8.1 | readonly class requires 8.2 | Use supported source runtime; do not ignore the declaration mismatch |
| Signature rejected | Actual release compatibility issue or tampering | Deny access and run the published vectors |
## 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)
# Python SDK
Canonical: https://licensing.kdpika.com/docs/v1/sdks/python
---
title: "Python SDK"
description: "Install kdpika-licensing from PyPI, configure signed licensing safely, and migrate from the older Python package."
slug: "/docs/v1/sdks/python"
section: "SDKs and languages"
order: 47
audience: "all"
level: "intermediate"
last_updated: "2026-10-21"
version: "v1"
related: ["/docs/v1/sdks/overview", "/docs/v1/sdks/upgrading", "/docs/v1/start/quickstart"]
draft: false
---
The official Python SDK is **kdpika-licensing 1.1.5**, published on [PyPI](https://pypi.org/project/kdpika-licensing/). Its [GitHub source](https://github.com/kdpika/kdpika-licensing) is MIT-licensed. Import **kdpika_licensing**, not the distribution name.
## What you will learn
- Install the published package and optional extras.
- Activate, validate, inspect verified [features](/docs/v1/start/glossary#feature) and release an installation.
- Preserve private state and migrate without creating another installation.
## Prerequisites
Create a [product](/docs/v1/start/glossary#product) and a disposable [Sandbox](/docs/v1/start/glossary#sandbox) [license](/docs/v1/start/glossary#license). Copy your [Public App ID](/docs/v1/start/glossary#public-app-id) from Integration. A [developer](/docs/v1/start/glossary#developer) management API key never belongs in a desktop application.
## Runtime and package
Python **3.11–3.14** is required. The dependency constraint is cryptography >=50.0.2,<51. Core release checks passed on Windows, Linux and Intel macOS across all four Python versions; a fresh PyPI installation and signed lifecycle also passed on Apple Silicon macOS. These checks do not certify every native GUI, keyring or packaged executable.
Apple Silicon macOS and supported 64-bit Windows/Linux normally use dependency wheels. Intel macOS may need Rust, Xcode command-line tools and OpenSSL 3 to build cryptography. See [upstream installation requirements](https://cryptography.io/en/latest/installation/). A newer Python version outside the declared range is not supported by this release.
## Install and use
Create a private virtual environment using a supported Python interpreter:
```bash
python -m venv .venv
source .venv/bin/activate
python -m pip install "kdpika-licensing==1.1.5"
python -m pip check
```
On Windows, create the environment with your supported Python launcher and activate it with .venv\\Scripts\\Activate.ps1 in PowerShell, or use .venv\\Scripts\\python.exe directly.
Optional extras are separate, not required for a core integration:
```bash
python -m pip install "kdpika-licensing[secure-storage]==1.1.5"
python -m pip install "kdpika-licensing[ui]==1.1.5"
```
secure-storage installs OS keyring support. ui installs PyQt6 for the optional [activation](/docs/v1/start/glossary#activation) UI. A usable native keyring still depends on the computer and OS configuration; do not describe private-file fallback as encryption.
Set KDP_APP_ID, KDP_STATE_DIR, KDP_LICENSE_KEY and optional KDP_CUSTOMER_EMAIL privately. The state directory must persist across restarts and upgrades.
```python
import os
from pathlib import Path
from kdpika_licensing import LicenseClient
client = LicenseClient(
product=os.environ["KDP_APP_ID"],
environment="SANDBOX",
base_url="https://licensing.kdpika.com",
storage_dir=Path(os.environ["KDP_STATE_DIR"]),
)
result = client.check()
if result.error_code == "NOT_ACTIVATED":
result = client.activate(
os.environ["KDP_LICENSE_KEY"],
os.getenv("KDP_CUSTOMER_EMAIL") or None,
)
if result.valid and result.signature_valid and result.feature("pdf_export"):
print("PDF export is permitted.")
else:
print("This feature is not available. Check your license or connection.")
```
pdf_export is an example entitlement: define it in your [product plan](/docs/v1/start/glossary#product-plan). When an email is supplied, the server requires it to match the license [customer](/docs/v1/start/glossary#customer). Do not automatically reactivate after every validation failure.
## Complete constructor configuration
| Option | Default | Purpose |
| --- | --- | --- |
| product | required | Public App ID |
| environment | production, normalized to [LIVE](/docs/v1/start/glossary#live) | Explicitly use SANDBOX for tests; TEST/PRODUCTION are aliases |
| base_url | https://licensing.kdpika.com | HTTPS origin; no path, query, credentials or fragment |
| application_name | Licensed Application | App label and default state path |
| application_version | empty string | Your app version, not the SDK version |
| verification_key | None | Optional trusted key_id/public_key pin |
| storage_dir | None | Private persistent root; SDK adds a product/environment namespace |
| timeout | 15.0 | Socket timeout above 0 and at most 120 seconds; not a total wall-time guarantee |
| prefer_keyring | True | Try the optional usable OS keyring |
| transport | None | Advanced trusted transport injection; preserve original JSON bytes |
| clock | None | Aware UTC clock; injection is for controlled tests |
| allow_loopback_http | False | Keyword-only local fixture opt-in; never use for remote HTTP |
Pins do not rotate silently. Fetch trusted keys only from the configured HTTPS origin or a controlled application update.
## Public members and result contract
| Member | Use |
| --- | --- |
| client.activate(key, email=None) | Register the current installation online |
| client.check(force_online=False) | Check eligible signed cache or validate online |
| client.validate() | Request a fresh signed decision; bounded offline fallback only for eligible transport failure |
| client.deactivate() | Release the installation online |
| client.reset_local() | Clear local credentials/cache; does not release a server seat |
| client.activated | Local activation-state indicator, not an authorization decision |
| client.installation_id | Stable persisted random installation identity |
| client.storage_security | Actual storage mode; do not claim a vault without checking |
| client.feature(name, default=False) | Recheck current state before boolean feature use |
| client.feature_value(name, default=None) | Recheck state before typed feature access |
| result.valid / result.signature_valid | Explicit decision and signature gates |
| result.feature / result.feature_value | Fail-closed feature helpers; still use the explicit validity gate |
| result.status / license_id / expires_at / offline_grace_until | Verified result metadata; display only what is present |
| result.error_code / message / request_id | Safe failure handling and support correlation |
For exact signatures, fields and advanced storage/transport members, read the [versioned client source](https://github.com/kdpika/kdpika-licensing/blob/v1.1.5/src/kdpika_licensing/client.py), [result source](https://github.com/kdpika/kdpika-licensing/blob/v1.1.5/src/kdpika_licensing/models.py) and [SDK documentation](https://github.com/kdpika/kdpika-licensing/tree/v1.1.5/docs). Do not treat writable payload data as an authorization grant.
## Runnable member examples
The [versioned examples directory](https://github.com/kdpika/kdpika-licensing/tree/v1.1.5/examples) contains basic.py, session.py, members.py and the optional PyQt6 example. Download the repository tag or source archive to get examples; a wheel installation is a library, not a promise that examples appear in your working directory.
Keep the adjacent _common.py helper when copying those examples. The persistent CLI supports status, activate, check, validate, deactivate and clear-local. Keep credentials in private environment variables rather than command-line arguments.
## Errors and exceptions
License operations return LicenseResult for expected refusals. ConfigurationError, StorageError, SignatureVerificationError and NetworkError derive from LicenseSdkError and describe SDK failure categories. Catch only what your application can handle and deny protected access on failure. Do not log raw tokens, keys, signed customer payloads or private storage paths.
## Storage and installation identity
Preserve the same product, environment, application name and private storage root. The SDK uses namespaced persistent random identity, atomic private files and bounded process/thread coordination. It does not derive identity from a MAC address or machine serial number. Local files are not protection from a computer owner or malware with equivalent access.
## Offline and clock policy
An online activation is required first. Offline acceptance re-verifies cached signatures, binding, status and signed validation/grace/expiry deadlines. Authoritative server refusals do not authorize offline fallback. Clock rollback and corrupt/missing state fail closed. Do not delete state to work around those refusals.
## Threading, asynchronous work and lifetime
AsyncLicenseClient wraps a configured LicenseClient and offloads its bounded synchronous operations. Await activate, check, validate and deactivate. Preserve the underlying client's private state. Follow the [async source contract](https://github.com/kdpika/kdpika-licensing/blob/v1.1.5/src/kdpika_licensing/async_client.py); a local thread lock is not a distributed lock.
## Application and framework integration
Run licensing checks in the desktop application, CLI, service or authenticated web backend. Never expose [developer API keys](/docs/v1/start/glossary#developer-api-key) or turn these helpers into an unrestricted browser management endpoint. Keep blocking calls off a GUI event loop.
## Packaging and distribution
Bundle only the dependencies your application needs. PyQt6 and native keyring backends require target-OS acceptance. Test your actual packaged app, state persistence, missing-vault behavior and offline/refusal paths. The SDK's MIT license does not license the KDPika SaaS backend or grant a [platform subscription](/docs/v1/start/glossary#platform-subscription).
## Upgrade and uninstall
Follow [the migration checklist](/docs/v1/sdks/upgrading). The old license_sdk import is deprecated but remains a compatibility shim. Remove an installed studio-soline-license-sdk distribution before installing the new one so two distributions do not own that namespace. Preserve private identity/state. Deactivate online before intentionally releasing a seat; uninstalling a package alone does not release it.
## Expected result
The package imports from your virtual environment and your application grants features only after an accepted signed decision.
## Common mistakes
- Installing a different similarly named package or using an unsupported Python version.
- Shipping developer API keys, hard-coded customer keys or synthetic example App IDs.
- Deleting identity/state during an upgrade or assuming token presence means paid access.
## Security notes
Keep HTTPS, signature verification, product/environment/installation binding and deadline checks enabled. Never bypass a failed signature to make an example pass. Pin the package version for reproducible application releases and review changes before upgrading.
## Troubleshooting
| Symptom | Safe next step |
| --- | --- |
| No matching distribution | Check Python 3.11–3.14 and the exact kdpika-licensing package name |
| Import fails | Use the same virtualenv interpreter for pip and your application; import kdpika_licensing |
| Intel macOS dependency build fails | Follow cryptography's Rust/OpenSSL/Xcode build requirements |
| NOT_ACTIVATED | Perform an explicit online activation with the correct App ID and environment |
| Signature or clock failure | Deny access; inspect trusted configuration and contact the publisher |
| Storage unavailable | Use the intended writable private directory; do not silently create another identity |
## Next steps
Run the [quickstart](/docs/v1/start/quickstart), inspect [API activation](/docs/v1/api/activate), and test [offline behavior](/docs/v1/security/offline-mode).
## Related
- [SDK overview and source checksums](/docs/v1/sdks/overview)
- [Upgrade safely](/docs/v1/sdks/upgrading)
- [Integration limitations](/docs/v1/integration-notes)
# PHP SDK
Canonical: https://licensing.kdpika.com/docs/v1/sdks/php
---
title: "PHP SDK"
description: "Install and configure the official PHP SDK with exact members, safe lifecycle examples, storage and release limitations."
slug: "/docs/v1/sdks/php"
section: "SDKs and languages"
order: 48
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 PHP 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
PHP 8.2+ for the actual readonly result class; cURL/JSON and native Sodium or Composer-loaded sodium_compat. composer.json still declares ^8.1; this guide does not change that mismatch. Clean install uses PHP8.5.4 and sodium_compat2.5.2.
Package kdpika/license-sdk, current label 1.0.0. Download kdpika-php-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
composer install --working-dir ./kdpika-php-sdk --no-interaction --no-plugins --no-scripts --prefer-dist
```
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.
```php
$client->activated(),'storage_security'=>'permission-protected-file',
'installation_hash'=>hash('sha256',$client->installationId())],JSON_THROW_ON_ERROR),PHP_EOL;exit;
}
if($action==='activate'){
$key=required('KDP_LICENSE_KEY');if(!preg_match('/^TEST[A-Z0-9]*-/i',$key))throw new RuntimeException('Use a Sandbox key');
$result=$client->activate($key,getenv('KDP_CUSTOMER_EMAIL')?:null);
}else $result=$client->$action();
$active=$action!=='deactivate'&&$result->valid&&$result->signatureValid;
$limit=$active?$result->featureValue('seat_count'):null;
echo json_encode(['valid'=>$result->valid,'source'=>$result->source,'error_code'=>$result->errorCode,
'signature_valid'=>$result->signatureValid,'pdf_export_allowed'=>$active&&$result->feature('pdf_export'),
'seat_count'=>is_int($limit)&&$limit>=0?$limit:null,
'request_id'=>preg_match('/^req_[a-f0-9]{24}$/D',$result->requestId??'')?$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.')],JSON_THROW_ON_ERROR),PHP_EOL;
exit($result->valid?0:1);
}catch(Throwable){
fwrite(STDERR,'License check unavailable. Review configuration, connectivity and private storage permissions. Do not disable signature verification.'.PHP_EOL);exit(2);
}
```
1. Run php session.php status. Expected: activated=false for fresh storage, with only an installation hash and storage category.
2. Run php session.php 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 php session.php 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 php session.php validate. Expected: forced-online path, subject to the documented bounded transport-failure fallback.
5. Run php session.php deactivate. Expected: the installation seat is released and local token/cache removed; pdf_export_allowed remains false for this release operation.
6. Run php session.php 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 | ?array | 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. |
| timeout | float seconds | 15 seconds / 15000 milliseconds | Transport timeout, not an [offline grace](/docs/v1/start/glossary#offline-grace) or [validation interval](/docs/v1/start/glossary#validation-interval). |
| transport | ?callable | null (built-in cURL) | Test injection must preserve the actual response/error contract; never accept unsigned fixtures in release. |
| clock | ?callable | 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 or 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.__construct | constructor | public function __construct( private readonly string $product, string $environment = 'LIVE', string $baseUrl = 'https://licensing.kdpika.com', private readonly string $applicationName = 'Licensed Application', private readonly string $applicationVersion = '', ?array $verificationKey = null, ?string $storageDir = null, private readonly float $timeout = 15.0, ?callable $transport = null, ?callable $clock = null, ) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseClient.installationId | method | public function installationId(): string | Persisted random installation identity; not a hardware fingerprint or secret [developer API key](/docs/v1/start/glossary#developer-api-key). |
| LicenseClient.activated | method | public function activated(): bool | Token-presence status only; not a current entitlement decision. |
| LicenseClient.activate | method | public function activate(string $licenseKey, ?string $email = null): LicenseResult | Send activation; return a valid signed result or explicit refusal. Success stores the token and signed state; verification failure attempts best-effort deactivation. |
| LicenseClient.check | method | public function check(bool $forceOnline = false): LicenseResult | Use verified cache when eligible or validate online. A transport failure can enter bounded offline policy; explicit server rejection is not offline permission. |
| LicenseClient.validate | method | public function validate(): LicenseResult | Force the check path online; transport failure can still attempt bounded verified offline fallback, not an unconditional online-only guarantee. |
| LicenseClient.deactivate | method | public function deactivate(): LicenseResult | Release this token seat online and clear token/cache on success. A successful release result does not grant access. Installation identity remains. |
| LicenseClient.feature | method | public function feature(string $name, bool $default = false): bool | Client helper may perform a new check but does not enforce valid itself. Prefer one accepted result, then result feature access. Never use the helper as the sole permission gate. |
| LicenseResult.__construct | constructor | public function __construct( public bool $valid, public string $source, public array $payload = [], public ?string $errorCode = null, public string $message = '', public ?string $requestId = null, public bool $signatureValid = false, public 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.feature | method | public function feature(string $name, bool $default = false): bool | 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 function featureValue(string $name, mixed $default = null): mixed | Read the typed feature value/default. Check valid and the actual feature type first. |
| LicenseResult.valid | field | public 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 | public 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 | public array $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 | public ?string $errorCode | 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 | public 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 | public ?string $requestId | 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 | public bool $signatureValid | 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 | public string $storageSecurity | 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 php members.php. 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.
```php
installationId(), 'lmp-install-'));
$check('LicenseClient.activated', !$client->activated());
$check('LicenseClient.activate', $client->activate('')->errorCode === 'INVALID_LICENSE');
$check('LicenseClient.check', $client->check()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.validate', $client->validate()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.deactivate', $client->deactivate()->errorCode === 'NOT_ACTIVATED');
$check('LicenseClient.feature', $client->feature('pdf_export') === false);
// A constructed result is NOT a signature verification or license decision.
$payload = ['features' => ['pdf_export' => true, 'seat_count' => 2]];
$result = new LicenseResult(false, 'example', $payload, 'EXAMPLE_ONLY', 'Example', 'req_example', false, 'unknown');
$check('LicenseResult.__construct', $result instanceof LicenseResult);
$expected = ['valid' => false, 'source' => 'example', 'payload' => $payload, 'errorCode' => 'EXAMPLE_ONLY',
'message' => 'Example', 'requestId' => 'req_example', 'signatureValid' => false, 'storageSecurity' => 'unknown'];
foreach ($expected as $name => $value) $check('LicenseResult.' . $name, $result->$name === $value);
$check('LicenseResult.feature', $result->feature('pdf_export') && !$result->feature('missing'));
$check('LicenseResult.featureValue', $result->featureValue('seat_count') === 2 && $result->featureValue('missing', 7) === 7);
$check('application.fail_closed', !($result->valid && $result->signatureValid && $result->feature('pdf_export')));
// Only this newly created scratch directory is cleaned; no user state is read.
foreach (new DirectoryIterator($root) as $directory) {
if ($directory->isDot()) continue;
foreach (new DirectoryIterator($directory->getPathname()) as $file) {
if (!$file->isDot()) unlink($file->getPathname());
}
rmdir($directory->getPathname());
}
rmdir($root);
echo json_encode(['result' => 'PASS', 'members' => $covered, 'mode' => 'synthetic local objects; no HTTP', 'access_granted' => false], JSON_THROW_ON_ERROR) . PHP_EOL;
```
## Errors and exceptions
Configuration/storage/transport/signature helpers use RuntimeException; normal online/local licensing errors return LicenseResult. A malformed custom transport or filesystem failure can escape a method. Wrap the application boundary, log only safe operation/time/code/request ID, and keep display_errors off on production. PHP8.5 curl_close deprecation is a known compatibility notice, not a signature bypass.
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 files: a namespaced0700 directory and atomic0600 token/cache/identity files where the OS supports those permissions. No automatic keyring/DPAPI/Keychain adapter is implemented. Do not put storageDir under public_html or share it between tenants. activated() means a token exists, not that the [license](/docs/v1/start/glossary#license) is currently valid.
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
The API is synchronous; no threading, event-loop or interprocess serialization is promised. Serialize activate/deactivate/identity writes for the same installation. On a shared backend, model your application tenant/installation explicitly instead of sharing one cache/token across all buyers.
## Application and framework integration
Laravel/Symfony can use a Composer path repository or a private source package for this same code, but a public Packagist release is not established. Create the client in your application service layer, not a public unauthenticated controller with arbitrary product/storage input. WordPress integrations must keep credentials/cache outside web-accessible paths, protect actions with actual authentication/authorization/CSRF controls and retain the same stable installation identity. No framework plugin is implied by this library.
## Packaging and distribution
Deploy the exact SDK source and Composer dependencies with ext-curl and Sodium capability. Keep vendor/server state private, lock dependency versions in your application and verify permissions under the actual PHP process user. Do not install a Node/Docker toolchain on shared hosting for this SDK. Native Sodium and compatibility fallback behavior need separate tests; a successful native run is not proof of fallback execution.
## 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
composer remove kdpika/license-sdk
```
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)
# JavaScript SDK
Canonical: https://licensing.kdpika.com/docs/v1/sdks/javascript
---
title: "JavaScript SDK"
description: "Install and configure the official JavaScript SDK with exact members, safe lifecycle examples, storage and release limitations."
slug: "/docs/v1/sdks/javascript"
section: "SDKs and languages"
order: 49
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 JavaScript 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
Node18+ ESM with native fetch/crypto/filesystem APIs; the tested session program targets Node18.20+ syntax and runs on Node22.22.2. Intended for Node services or Electron main process, not ordinary browser JavaScript.
Package @kdpika/license-sdk, current label 1.0.0. Download kdpika-javascript-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
npm install --ignore-scripts --save-exact ./kdpika-javascript-sdk
```
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.
```javascript
/** Sandbox session CLI; Node18.20+, official local SDK1.0.0 (ESM).
* Run node session.mjs status|activate|check|validate|deactivate.
* Set KDP_APP_ID,KDP_STATE_DIR; activate: KDP_LICENSE_KEY/optional customer email.
* Expected safe JSON, no raw signed payload/activation token. Keep validity gate.
*/
import crypto from 'node:crypto';
import {LicenseClient} from '@kdpika/license-sdk';
function required(name){const value=(process.env[name]||'').trim();if(!value)throw Error('Missing configuration');return value;}
try{
const action=process.argv.length===3?process.argv[2]:'';
if(!['status','activate','check','validate','deactivate'].includes(action))throw Error('Choose an action');
const base=(process.env.KDP_BASE_URL||'https://licensing.kdpika.com').replace(/\/+$/,'');const uri=new URL(base);
const local=uri.protocol==='http:'&&['127.0.0.1','localhost'].includes(uri.hostname)&&process.env.KDP_ALLOW_LOOPBACK==='1';
if(uri.username||uri.password||uri.pathname!=='/'||uri.search||uri.hash||(uri.protocol!=='https:'&&!local))throw Error('Use an HTTPS site origin');
const client=new LicenseClient({product:required('KDP_APP_ID'),environment:'SANDBOX',baseUrl:base,
applicationName:'KDPika SDK session example',applicationVersion:'1.0.0',storageDir:required('KDP_STATE_DIR')});
if(action==='status')console.log(JSON.stringify({activated:client.activated,storage_security:'permission-protected-file',
installation_hash:crypto.createHash('sha256').update(client.installationId).digest('hex')}));
else{
let result;
if(action==='activate'){
const key=required('KDP_LICENSE_KEY');if(!/^TEST[A-Z0-9]*-/i.test(key))throw Error('Use a Sandbox key');
result=await client.activate(key,process.env.KDP_CUSTOMER_EMAIL||null);
}else result=await client[action]();
const active=action!=='deactivate'&&result.valid&&result.signatureValid;
const limit=active?result.featureValue('seat_count'):null;
console.log(JSON.stringify({valid:result.valid,source:result.source,error_code:result.errorCode,
signature_valid:result.signatureValid,pdf_export_allowed:Boolean(active&&result.feature('pdf_export')),
seat_count:Number.isSafeInteger(limit)&&limit>=0?limit:null,request_id:/^req_[a-f0-9]{24}$/.test(result.requestId||'')?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.'}));
if(!result.valid)process.exitCode=1;
}
}catch{
console.error('License check unavailable. Review configuration, connectivity and private storage permissions. Do not disable signature verification.');process.exitCode=2;
}
```
1. Run node session.mjs status. Expected: activated=false for fresh storage, with only an installation hash and storage category.
2. Run node session.mjs 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 node session.mjs 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 node session.mjs validate. Expected: forced-online path, subject to the documented bounded transport-failure fallback.
5. Run node session.mjs deactivate. Expected: the installation seat is released and local token/cache removed; pdf_export_allowed remains false for this release operation.
6. Run node session.mjs 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/array/object | null/None | Optional pinned key with key_id and raw base64url public_key; manual rotation. |
| storageDir | path/string | OS-derived application storage root | Namespace includes origin, product and environment; keep outside public web roots. |
| timeoutMs | seconds / milliseconds | 15000 milliseconds | Transport timeout, not an [offline grace](/docs/v1/start/glossary#offline-grace) or [validation interval](/docs/v1/start/glossary#validation-interval). |
| fetchImpl | fetch-compatible function | globalThis.fetch | Test injection must preserve the actual response/error contract; never accept unsigned fixtures in release. |
| clock | callable | 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 or 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 |
| --- | --- | --- | --- |
| LicenseResult.constructor | constructor | constructor({valid, source, payload = {}, errorCode = null, message = "", requestId = null, signatureValid = false, 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.feature | method | feature(name, 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 | featureValue(name, fallback = null) | Read the typed feature value/default. Check valid and the actual feature type first. |
| LicenseClient.constructor | constructor | constructor({product, environment = "LIVE", baseUrl = "https://licensing.kdpika.com", applicationName = "Licensed Application", applicationVersion = "", verificationKey = null, storageDir = null, timeoutMs = 15000, fetchImpl = globalThis.fetch, clock = () => new Date()}) | Configure this type using the exact signature. Client construction creates/loads its local installation namespace; result construction alone is not verification. |
| LicenseClient.activated | property | get activated() | Token-presence status only; not a current entitlement decision. |
| LicenseClient.activate | method | async activate(licenseKey, email = null) | Send activation; return a valid signed result or explicit refusal. Success stores the token and signed state; verification failure attempts best-effort deactivation. |
| LicenseClient.check | method | async check(forceOnline = false) | Use verified cache when eligible or validate online. A transport failure can enter bounded offline policy; explicit server rejection is not offline permission. |
| LicenseClient.validate | method | validate() | Force the check path online; transport failure can still attempt bounded verified offline fallback, not an unconditional online-only guarantee. |
| LicenseClient.deactivate | method | async deactivate() | Release this token seat online and clear token/cache on success. A successful release result does not grant access. Installation identity remains. |
| LicenseClient.feature | method | async feature(name, fallback = false) | Client helper may perform a new check but does not enforce valid itself. Prefer one accepted result, then result feature access. Never use the helper as the sole permission gate. |
| LicenseClient.offline | method | async offline() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.verified | method | async verified(signed, source, response = null) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.getVerificationKey | method | async getVerificationKey(signed) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.saveState | method | async saveState(signed) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.checkpoint | method | checkpoint() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.token | method | token() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.bestEffortDeactivate | method | async bestEffortDeactivate(token) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.request | method | async request(method, endpoint, body, headers) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.responseError | method | responseError(response) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.error | method | error(errorCode,message,source,response=null,payload={},signatureValid=false) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.now | method | now() | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.readJson | method | readJson(name) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.writeJson | method | writeJson(name,value) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.atomicWrite | method | atomicWrite(name,value) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| LicenseClient.remove | method | remove(name) | Language-public implementation helper. Not a supported permission shortcut; requires trusted arguments and the same signature/storage/time rules. Keep direct calls inside isolated conformance tests, not renderer or buyer-controlled interfaces. |
| module.canonicalJson | function | export function canonicalJson(value) | Exported current canonicalizer, with the documented integral-float limitation. Not a standalone verification or entitlement decision. |
| LicenseResult.valid | field | 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 | 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 | 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 | errorCode | 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 | 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 | requestId | 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 | signatureValid | 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 | storageSecurity | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.product | attribute | product | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.environment | attribute | environment | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.baseUrl | attribute | baseUrl | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.applicationName | attribute | applicationName | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.applicationVersion | attribute | applicationVersion | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.verificationKey | attribute | verificationKey | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.timeoutMs | attribute | timeoutMs | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.fetchImpl | attribute | fetchImpl | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.clock | attribute | clock | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.storageDir | attribute | storageDir | Actual exposed value/component; inspect the declared type. Do not treat writable/internal state or result payload presence as an authorization grant. |
| LicenseClient.installationId | attribute | installationId | Persisted random installation identity; not a hardware fingerprint or secret [developer API key](/docs/v1/start/glossary#developer-api-key). |
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 node members.mjs. 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.
```javascript
// Isolated public-member conformance exercise, NOT a production access client.
// The ephemeral key signs synthetic in-memory data for an injected transport.
// It is never trusted by KDPika, written to disk, printed or used on the network.
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import {LicenseClient, LicenseResult, canonicalJson} from "@kdpika/license-sdk";
const covered = [];
const check = (member, condition) => { assert.ok(condition, `Member example failed: ${member}`); covered.push(member); };
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "kdpika-members-"));
fs.chmodSync(temporary, 0o700);
try {
let now = new Date("2026-10-17T12:00:00Z"), mode = "online", releaseCalls = 0;
const pair = crypto.generateKeyPairSync("ed25519");
const publicBytes = pair.publicKey.export({type: "spki", format: "der"}).subarray(-32);
const key = {key_id: "ed25519_example", algorithm: "Ed25519", public_key: publicBytes.toString("base64url")};
let client;
const signed = () => {
const payload = {product_id: "YOUR_APP_ID", environment: "sandbox", status: "active",
device: {installation_hash: crypto.createHash("sha256").update(client.installationId).digest("hex")},
features: {pdf_export: true, seat_count: 2}, server_time: "2026-10-17T12:00:00Z",
next_validation_at: "2026-10-18T12:00:00Z", offline_grace_until: "2026-10-20T12:00:00Z",
expires_at: "2026-11-17T12:00:00Z"};
return {payload, signature: crypto.sign(null, Buffer.from(canonicalJson(payload)), pair.privateKey).toString("base64url"),
algorithm: "Ed25519", key_id: key.key_id};
};
const fetchImpl = async (url, options) => {
const endpoint = new URL(url).pathname;
assert.equal(new URL(url).origin, "https://licenses.example.invalid");
assert.ok(options.signal instanceof AbortSignal);
if (mode === "network") throw new Error("Synthetic transport outage");
let body;
if (endpoint === "/api/v1/public-key") body = {success: true, key};
else if (endpoint === "/api/v1/licenses/deactivate") { releaseCalls++; body = {success: true, deactivated: true}; }
else if (mode === "rejected") return {ok: false, status: 403, json: async () => ({success: false, error: {code: "LICENSE_REVOKED", message: "Example refusal"}})};
else body = {success: true, activation_token: "UNUSABLE_EXAMPLE_TOKEN", license: signed(), request_id: "req_example"};
// The actual SDK deliberately requires act_ at activation. This unusable,
// local-only test value cannot be accepted by a real KDPika token lookup.
if (endpoint === "/api/v1/licenses/activate") body.activation_token = "act_example_not_a_real_credential";
return {ok: true, status: 200, json: async () => body};
};
client = new LicenseClient({product: "YOUR_APP_ID", environment: "SANDBOX", baseUrl: "https://licenses.example.invalid",
storageDir: temporary, verificationKey: key, fetchImpl, clock: () => now});
check("LicenseClient.constructor", client instanceof LicenseClient);
for (const name of ["product", "environment", "baseUrl", "applicationName", "applicationVersion", "verificationKey",
"timeoutMs", "fetchImpl", "clock", "storageDir", "installationId"]) check(`LicenseClient.${name}`, client[name] !== undefined);
check("LicenseClient.activated", !client.activated);
check("LicenseClient.token", client.token() === null);
check("LicenseClient.now", client.now().toISOString() === now.toISOString());
check("module.canonicalJson", canonicalJson({b: [1, true], a: "value"}) === '{"a":"value","b":[1,true]}');
const example = new LicenseResult({valid: false, source: "example", payload: {features: {pdf_export: true, seat_count: 2}},
errorCode: "EXAMPLE_ONLY", message: "Example", requestId: "req_example", signatureValid: false, storageSecurity: "unknown"});
check("LicenseResult.constructor", Object.isFrozen(example));
for (const [name, expected] of Object.entries({valid: false, source: "example", errorCode: "EXAMPLE_ONLY", message: "Example",
requestId: "req_example", signatureValid: false, storageSecurity: "unknown"})) check(`LicenseResult.${name}`, example[name] === expected);
check("LicenseResult.payload", example.payload.features.pdf_export === true);
check("LicenseResult.feature", example.feature("pdf_export") && !example.feature("missing"));
check("LicenseResult.featureValue", example.featureValue("seat_count") === 2 && example.featureValue("missing", 7) === 7);
check("application.fail_closed", !(example.valid && example.signatureValid && example.feature("pdf_export")));
check("LicenseClient.error", client.error("EXAMPLE_ONLY", "Example", "local").valid === false);
check("LicenseClient.responseError", client.responseError({body: {error: {code: "INVALID_TOKEN", message: "Example"}}}).errorCode === "INVALID_TOKEN");
const response = await client.request("GET", "/api/v1/public-key", null, {});
check("LicenseClient.request", response.ok && response.status === 200 && response.body.key.algorithm === "Ed25519");
check("LicenseClient.getVerificationKey", (await client.getVerificationKey(signed())).key_id === key.key_id);
check("LicenseClient.verified", (await client.verified(signed(), "online")).signatureValid);
await client.saveState(signed());
check("LicenseClient.saveState", client.readJson("signed-state.json").signed_license.algorithm === "Ed25519");
client.checkpoint();
check("LicenseClient.checkpoint", client.readJson("signed-state.json").last_seen_at === now.toISOString());
check("LicenseClient.offline", (await client.offline()).valid);
client.writeJson("example.json", {local_example: true});
check("LicenseClient.writeJson", fs.existsSync(path.join(client.storageDir, "example.json")));
check("LicenseClient.readJson", client.readJson("example.json").local_example === true);
client.atomicWrite("example.txt", "unusable local text");
check("LicenseClient.atomicWrite", fs.readFileSync(path.join(client.storageDir, "example.txt"), "utf8") === "unusable local text");
client.remove("example.txt");
check("LicenseClient.remove", !fs.existsSync(path.join(client.storageDir, "example.txt")));
const activated = await client.activate("TEST-UNUSABLE-EXAMPLE", "synthetic@example.invalid");
check("LicenseClient.activate", activated.valid && activated.signatureValid && client.activated);
check("LicenseClient.check", (await client.check()).source === "cache");
check("LicenseClient.validate", (await client.validate()).source === "online");
check("LicenseClient.feature", await client.feature("pdf_export"));
const beforeRelease = releaseCalls;
await client.bestEffortDeactivate("UNUSABLE_EXAMPLE_TOKEN");
check("LicenseClient.bestEffortDeactivate", releaseCalls === beforeRelease + 1);
now = new Date("2026-10-18T13:00:00Z"); mode = "network";
assert.equal((await client.validate()).source, "offline");
now = new Date("2026-10-21T12:00:00Z");
assert.equal((await client.validate()).errorCode, "OFFLINE_GRACE_EXPIRED");
now = new Date("2026-10-17T11:54:59Z");
assert.equal((await client.offline()).errorCode, "CLOCK_ROLLBACK_DETECTED");
now = new Date("2026-10-19T12:00:00Z"); mode = "rejected";
assert.equal((await client.validate()).errorCode, "LICENSE_REVOKED");
assert.equal(client.activated, false);
mode = "online";
assert.equal((await client.activate("TEST-UNUSABLE-EXAMPLE")).valid, true);
const released = await client.deactivate();
check("LicenseClient.deactivate", released.valid && !released.signatureValid && !client.activated);
console.log(JSON.stringify({result: "PASS", members: covered, mode: "injected synthetic signing/transport; zero HTTP",
access_granted: false, additional_cases: ["offline", "grace-expired", "clock-rollback", "explicit-revocation"]}));
} finally {
// Only the exact fresh scratch directory from mkdtempSync is removed.
fs.rmSync(temporary, {recursive: true});
}
```
## Errors and exceptions
Constructor validation throws Error. Lifecycle methods normally resolve LicenseResult; storage/custom-fetch/unexpected JSON failures can reject a Promise. Catch at your boundary and never show raw error text or payload. No built-in application logger or trace-redaction pipeline exists. The request helper uses AbortController; transport throws enter the documented offline/refusal path.
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 files in a namespaced directory, with0700/0600 attempts where supported. No built-in Electron safeStorage, Windows Credential Manager or Secret Service adapter. Keep tokens in the main process and use the [storage hardening guide](/docs/v1/security/secure-token-storage). A JavaScript property or local payload is not trusted by itself.
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
Async methods return Promises, but filesystem operations are synchronous and there is no shared-instance/thread/process locking. Serialize lifecycle operations and do not race activation/deactivation across workers. Node worker threads and multiple Electron instances must not write one namespace concurrently without your own coordination.
## Application and framework integration
Electron: construct the client only in the main process, keep contextIsolation enabled, expose narrow application actions through preload/IPC, and never return tokens/raw cache/arbitrary SDK methods to the renderer. The package is ESM only; CommonJS callers can use awaited dynamic import in an async entry point, not assume require() is an official CJS export. A web frontend must use its own authenticated backend and must not receive lmp_ [developer](/docs/v1/start/glossary#developer) keys.
## Packaging and distribution
The complete Electron app uses pinned Electron44.9.0/electron-builder26.15.3 and includes the exact local SDK directory. npm ci restores the locked project; npm start runs the source window and npm run package builds its directory artifact. Both source and unsigned macOS packaged windows pass real Sandbox lifecycle/offline/refusal tests. Keep storageDir outside ASAR/installation paths. Code signing, notarization, other native OS packages and real safeStorage backends remain separate deployment tests; this SDK does not automatically encrypt its token files.
## 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
npm uninstall @kdpika/license-sdk
```
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)
# API reference
Canonical: https://licensing.kdpika.com/docs/v1/api
---
slug: "/docs/v1/api"
title: "API reference"
description: "Find real API v1 endpoints, errors, contract tools and signing requirements without following a tutorial."
section: "API reference"
order: 50
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "hub"
---
## What you can do here
Find real API v1 endpoints, errors, contract tools and signing requirements without following a tutorial.
- [API conventions](/docs/v1/api/conventions) — Use KDPika API v1 with the correct credentials, JSON envelopes, [request IDs](/docs/v1/start/glossary#request-id), environment boundaries and safe retry assumptions.
- [Retrieve the public key](/docs/v1/api/public-key) — Retrieve the current [public verification key](/docs/v1/start/glossary#public-verification-key).
- [Create a license](/docs/v1/api/create-license) — Issue a license after your backend verifies the buyer purchase.
- [Activate an installation](/docs/v1/api/activate) — Bind a [customer](/docs/v1/start/glossary#customer) license to one persisted installation.
- [Validate an installation](/docs/v1/api/validate) — Refresh the signed state of an activated installation.
- [Deactivate an installation](/docs/v1/api/deactivate) — Release this installation [device](/docs/v1/start/glossary#device) seat.
- [Retrieve a license](/docs/v1/api/get-license) — Read a scoped unsigned license summary.
- [Revoke a license](/docs/v1/api/revoke) — Revoke a license and its active tokens.
- [Suspend a license](/docs/v1/api/suspend) — Suspend an eligible license and invalidate its active tokens.
- [Restore a license](/docs/v1/api/restore) — Restore an eligible license without reviving old tokens.
- [Renew a license](/docs/v1/api/renew) — Extend license expiry without charging or changing its plan.
- [Reset license devices](/docs/v1/api/reset-devices) — Release [active devices](/docs/v1/start/glossary#active-devices) and revoke their tokens.
- [Errors and safe user messages](/docs/v1/api/errors) — Handle every current licensing error, distinguish SDK-local and operational codes, and give users a safe next step.
- [Rate limits and bounded retries](/docs/v1/api/rate-limits) — Use exact licensing API windows and headers, avoid request loops, and distinguish rate limits from monthly usage allowances.
- [Idempotency and uncertain outcomes](/docs/v1/api/idempotency) — Choose stable creation keys, understand actual 24-hour replay limits, and avoid repeating uncertain license mutations.
- [OpenAPI specification](/docs/v1/api/openapi) — Download the complete source-validated OpenAPI 3.1 licensing contract, including actual statuses, errors, headers and examples.
- [Contract testing](/docs/v1/api/contract-testing) — Test every licensing operation and error shape against OpenAPI using a safe isolated [Sandbox](/docs/v1/start/glossary#sandbox) fixture and reproducible checks.
- [API client collections](/docs/v1/api/collections) — Download Postman, Insomnia and Bruno collections with Sandbox variables, assertions and a complete11-operation test sequence.
- [Try the Sandbox API](/docs/v1/api/try-it) — Inspect real same-origin Sandbox licensing requests without storing credentials, using [Live](/docs/v1/start/glossary#live) keys or requiring a [developer](/docs/v1/start/glossary#developer) account to read.
- [API v1 changelog](/docs/v1/api/api-changelog) — Track source-verifiable API documentation snapshots separately from [product](/docs/v1/start/glossary#product) releases and SDK versions.
- [Versioning and deprecation](/docs/v1/api/versioning-and-deprecation) — Understand API v1 compatibility boundaries, current notification limitations and a practical upgrade checklist.
## Next steps
[Follow the Complete Integration Guide](/docs/v1/integration-guide)
# C# SDK
Canonical: https://licensing.kdpika.com/docs/v1/sdks/csharp
---
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
Exenet7.0enableenablefalse
```
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? | 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? | 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? verificationKey = null, string? storageDir = null, HttpClient? httpClient = null, Func? 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 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 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 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 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();
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
Exenet7.0enableenablefalse
```
## 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)
# Sell and automate
Canonical: https://licensing.kdpika.com/docs/v1/sell
---
slug: "/docs/v1/sell"
title: "Sell and automate"
description: "Choose manual issuance, WooCommerce or a verified backend to deliver licenses after confirmed purchases."
section: "Sell and automate"
order: 51
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "hub"
---
## What you can do here
Choose manual issuance, WooCommerce or a verified backend to deliver [licenses](/docs/v1/start/glossary#license) after confirmed purchases.
Start with [manual issuance](/docs/v1/guides/manual-issuance) if you only want to send keys. Use the native WooCommerce connector for its combined import; advanced provider recipes require a trusted verification backend. KDPika platform billing is separate from your [customer](/docs/v1/start/glossary#customer)'s purchase.
- [Set up webhooks](/docs/v1/guides/webhooks-overview) — Create isolated signed notification endpoints, store their secrets and test [delivery](/docs/v1/start/glossary#delivery) without using notifications as license authority.
- [Webhook event catalog](/docs/v1/guides/webhook-events) — All eleven notification events, captured [Sandbox](/docs/v1/start/glossary#sandbox) payloads, field definitions, triggers and safe receiver actions.
- [Webhook delivery and retries](/docs/v1/guides/webhook-delivery) — Exact headers, HMAC bytes, attempt limits, delivery history, ordering, duplicate handling and manual retry limitations.
- [Verify webhook signatures](/docs/v1/guides/verify-webhook-signatures) — Execute raw-byte HMAC verifiers in Node, Python, PHP, C#, Go, Java and Ruby with positive and negative notification tests.
- [Test webhooks locally](/docs/v1/guides/test-webhooks-locally) — Run a private local notification inbox, test signed replay and duplicate protection, then inspect real staging deliveries.
- [Sell with Stripe](/docs/v1/guides/stripe) — Verified Stripe purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Sell with Paddle Billing](/docs/v1/guides/paddle) — Verified Paddle Billing purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Sell with Lemon Squeezy](/docs/v1/guides/lemon-squeezy) — Verified Lemon Squeezy purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Sell with Gumroad](/docs/v1/guides/gumroad) — Verified Gumroad purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [WooCommerce legacy service reference](/docs/v1/guides/woocommerce/legacy-service) — Verified WooCommerce / WordPress purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [KDPika Licensing for WooCommerce](/docs/v1/guides/woocommerce) — Install the official plugin, connect your workspace and deliver software licenses through your WooCommerce store.
- [Sell with Shopify](/docs/v1/guides/shopify) — Verified Shopify purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Sell with PayPal](/docs/v1/guides/paypal) — Verified PayPal purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Sell with FastSpring](/docs/v1/guides/fastspring) — Verified FastSpring purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries.
- [Connect your own store backend](/docs/v1/guides/any-store) — Map verified store purchases to durable licensing commands with four tested reference servers and conservative unknown-result handling.
- [Use no-code tools with the REST API](/docs/v1/guides/no-code) — Configure reviewed REST issuance in Zapier, Make or n8n, with private credentials, explicit result checks and honest retry/history limits.
- [Issue and deliver licenses manually](/docs/v1/guides/manual-issuance) — Issue dashboard licenses, run a safe CSV API batch, find customers and deliver saved keys without inventing import or key-recovery [features](/docs/v1/start/glossary#feature).
- [Licensing recipes and supported limits](/docs/v1/guides/recipes) — Fourteen tested licensing workflows, with explicit limits for plan migration, version gates, trials, [device](/docs/v1/start/glossary#device) seats, expiry and key delivery.
- [Choose a licensing architecture](/docs/v1/guides/architectures) — Trust boundaries, flows and trade-offs for desktop websites, SaaS backends, plugin marketplaces and CLI tools.
- [Troubleshoot webhook notifications](/docs/v1/guides/webhook-troubleshooting) — Diagnose routing gaps, HMAC failures, stale requests, duplicates, delivery timeouts and exhausted retries using verified sender behavior.
- [Run the store integration service](/docs/v1/guides/store-service) — Run the store integration service for the complete private reference application.
## Next steps
[Follow the Complete Integration Guide](/docs/v1/integration-guide)
# TypeScript REST client
Canonical: https://licensing.kdpika.com/docs/v1/sdks/typescript
---
title: "TypeScript REST client"
description: "Build a complete Sandbox session client with lossless signature verification, persistent identity, bounded offline policy and safe feature gating."
slug: "/docs/v1/sdks/typescript"
section: "SDKs and languages"
order: 51
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/security/verify-ed25519/javascript", "/docs/v1/api/activate", "/docs/v1/security/offline-mode"]
draft: false
---
Use the existing API v1 directly from a trusted Node service, CLI or Electron main process. This complete REST example is not an additional [official SDK](/docs/v1/start/glossary#official-sdk) or a browser licensing library.
## What you will learn
- Activate, check, validate and deactivate one stable [Sandbox](/docs/v1/start/glossary#sandbox) installation.
- Authenticate the exact signed JSON bytes without losing integral-float spellings.
- Enforce binding, status, start, expiry, grace and rollback policy before [features](/docs/v1/start/glossary#feature).
- Keep tokens private and distinguish transport failure from explicit rejection.
## Prerequisites
- Complete the [Sandbox quickstart](/docs/v1/start/quickstart).
- Use Node 22+, TypeScript 7.0.2 and @types/node 22.19.7. Actual execution is macOS/Node 22.22.2, not native Windows/Linux certification.
- Obtain a Sandbox [Public App ID](/docs/v1/start/glossary#public-app-id), a Sandbox [license](/docs/v1/start/glossary#license) and its matching email when using email-bound [activation](/docs/v1/start/glossary#activation). Never put an lmp_ [developer](/docs/v1/start/glossary#developer) key in this application.
- Obtain the actual public-key document from [GET /api/v1/public-key](/docs/v1/api/public-key) over your independently trusted HTTPS origin. Pin that document through your application configuration or controlled release process. Do not accept a public key supplied alongside an untrusted license envelope.
- Choose an absolute private state directory outside public web roots and the application installation/ASAR directory.
## Project setup and pinned dependencies
Preserve this layout. Both JavaScript files are the complete, already published verification sources, not replacements for the official SDK canonicalizer.
```text
samples/
sdks/
typescript/
client.ts
package.json
package-lock.json
security/
javascript/
canonical.mjs
verify.mjs
```
```json
{
"name": "kdpika-typescript-rest-example",
"private": true,
"version": "1.0.0",
"type": "module",
"engines": {"node": ">=22"},
"devDependencies": {"@types/node": "22.19.7", "typescript": "7.0.2"}
}
```
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
[kdpika-typescript-rest-example-1.0.0.zip](/docs/downloads/kdpika-typescript-rest-example-1.0.0.zip) · 10688 bytes · version 1.0.0
SHA-256: b8411aa54bfeca668d65473003b5680d125a7528a119708c579c96648e855cb8
From the TypeScript project directory, install the pinned development tools. No runtime npm dependency is needed by the client. Node/compiler installation is on your development machine, not KDPika's shared PHP hosting.
```bash
npm ci --ignore-scripts --no-audit --no-fund
npx --no-install tsc --strict --allowJs --checkJs false --target es2022 --module nodenext --moduleResolution nodenext --types node --outDir ./build client.ts
```
Expected: strict compilation with no diagnostics. The executable is build/sdks/typescript/client.js; keep it with build/security/javascript and the project module metadata. The documentation harness verifies this exact clean standalone layout, lockfile installation and compilation, then executes that compiled client against the isolated Sandbox. No downloadable TypeScript archive is claimed before the complete phase publication gate.
## Complete session client
```typescript
// Complete Sandbox REST session client, not an additional official SDK.
// Node22+, TypeScript7.0.2, @types/node22.19.7; no runtime npm dependencies.
// Keep the adjacent published security/javascript modules in their layout.
import * as fs from "node:fs";
import * as path from "node:path";
import {createHash, randomBytes, randomUUID} from "node:crypto";
import {verifyEnvelope} from "../../security/javascript/verify.mjs";
import {parseRaw, canonical} from "../../security/javascript/canonical.mjs";
type ObjectValue = Record;
type State = {token: string; envelope: string | null; checkpoint: string};
type Decision = {valid: boolean; source: string; signature_valid: boolean; error_code: string | null;
request_id: string | null; pdf_export_allowed: boolean; seat_count: number | null; message: string};
class Refusal extends Error {
constructor(readonly code: string, readonly requestId: string | null = null) {super(code);}
}
class Unreachable extends Error {}
const decode = (bytes: Uint8Array): unknown => parseRaw(bytes);
const object = (value: unknown): ObjectValue => {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Refusal("INVALID_RESPONSE");
return value as ObjectValue;
};
const time = (value: unknown): number => {
if (typeof value !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(value)) throw new Refusal("INVALID_TIME");
const result = Date.parse(value);
if (!Number.isFinite(result)) throw new Refusal("INVALID_TIME");
return result;
};
const friendly = (code: string): string => {
if (code === "INVALID_LICENSE") return "Check your license key and the email used for this license.";
if (code === "NOT_ACTIVATED") return "Activate this installation to continue.";
if (["NETWORK_ERROR", "OFFLINE_GRACE_EXPIRED"].includes(code)) return "Connect to the internet and try again.";
if (code === "CLOCK_ROLLBACK_DETECTED") return "Check your device date and time, then try again.";
return "Your license could not be confirmed. Please contact your software provider.";
};
const denied = (code: string, source: string, requestId: string | null = null): Decision => ({valid: false, source,
signature_valid: false, error_code: code, request_id: requestId, pdf_export_allowed: false, seat_count: null, message: friendly(code)});
class Session {
readonly directory: string;
readonly installation: string;
private readonly trusted: Buffer;
private readonly api: string;
private readonly app: string;
private readonly lock: string;
private readonly lockHandle: number;
constructor() {
this.app = process.env.KDP_APP_ID ?? "";
if (!/^[A-Za-z0-9_-]{3,160}$/.test(this.app)) throw new Error("Set KDP_APP_ID.");
const rawBase = process.env.KDP_BASE_URL ?? "https://licensing.kdpika.com/api/v1";
const base = new URL(rawBase);
const local = base.protocol === "http:" && ["127.0.0.1", "localhost"].includes(base.hostname) && process.env.KDP_ALLOW_LOOPBACK === "1";
if ((base.protocol !== "https:" && !local) || base.username || base.password || base.search || base.hash || base.pathname.replace(/\/$/, "") !== "/api/v1") throw new Error("Use the HTTPS API URL; only explicit loopback HTTP is allowed.");
this.api = base.origin + "/api/v1";
const root = process.env.KDP_STATE_DIR ?? "";
const trustedFile = process.env.KDP_TRUSTED_KEY_FILE ?? "";
if (!path.isAbsolute(root) || !path.isAbsolute(trustedFile)) throw new Error("Set absolute private state and independently trusted key paths.");
this.trusted = this.readFile(trustedFile);
const key = object(decode(this.trusted));
if (key.algorithm !== "Ed25519" || key.encoding !== "base64url") throw new Error("Use the actual trusted public-key document.");
const namespace = createHash("sha256").update(this.api + "|" + this.app + "|SANDBOX|typescript-rest-v1").digest("hex").slice(0, 20);
fs.mkdirSync(root, {recursive: true, mode: 0o700});
if (fs.lstatSync(root).isSymbolicLink()) throw new Error("State must not be a symlink.");
this.directory = path.join(root, namespace);
fs.mkdirSync(this.directory, {mode: 0o700, recursive: true});
if (fs.lstatSync(this.directory).isSymbolicLink()) throw new Error("Namespace must not be a symlink.");
fs.chmodSync(this.directory, 0o700);
this.lock = path.join(this.directory, ".lock");
this.lockHandle = fs.openSync(this.lock, "wx", 0o600);
try {
const identity = path.join(this.directory, "installation.json");
if (!fs.existsSync(identity)) this.write("installation.json", {installation_id: "lmp-install-" + randomUUID()});
const value = object(JSON.parse(this.readFile(identity).toString("utf8"))).installation_id;
if (typeof value !== "string" || !/^lmp-install-[0-9a-f-]{36}$/.test(value)) throw new Error("Installation identity is invalid; do not reset automatically.");
this.installation = value;
} catch (error) {this.close(); throw error;}
}
close(): void {fs.closeSync(this.lockHandle); fs.unlinkSync(this.lock);}
private readFile(file: string): Buffer {
const stat = fs.lstatSync(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576) throw new Error("Unsafe state/key file.");
return fs.readFileSync(file);
}
private write(name: string, value: unknown): void {
const target = path.join(this.directory, name), temporary = target + "." + randomBytes(12).toString("hex");
fs.writeFileSync(temporary, JSON.stringify(value), {flag: "wx", mode: 0o600});
fs.renameSync(temporary, target);
}
private state(): State | null {
const file = path.join(this.directory, "session.json");
if (!fs.existsSync(file)) return null;
const value = object(JSON.parse(this.readFile(file).toString("utf8")));
if (typeof value.token !== "string" || !/^act_[A-Za-z0-9_-]+$/.test(value.token) ||
!(value.envelope === null || typeof value.envelope === "string") || typeof value.checkpoint !== "string") throw new Error("Stored session is invalid.");
return value as State;
}
private clear(): void {
const file = path.join(this.directory, "session.json");
if (fs.existsSync(file)) {this.readFile(file); fs.unlinkSync(file);}
}
private async request(endpoint: string, body: ObjectValue, token?: string): Promise<{body: ObjectValue; requestId: string | null}> {
const headers: Record = {Accept: "application/json", "Content-Type": "application/json", "User-Agent": "KDPika-REST-TypeScript-example/1.0"};
if (token) {headers.Authorization = "Bearer " + token; headers["X-Installation-ID"] = this.installation;}
else {headers["X-App-ID"] = this.app; headers["X-Environment"] = "SANDBOX";}
let response: Response, raw: Buffer;
try {
response = await fetch(this.api + endpoint, {method: "POST", headers, body: JSON.stringify(body), redirect: "error", signal: AbortSignal.timeout(15000)});
const reader = response.body?.getReader(); if (!reader) throw new Refusal("INVALID_RESPONSE");
const chunks: Uint8Array[] = []; let size = 0;
for (;;) {
const next = await reader.read(); if (next.done) break;
size += next.value.length;
if (size > 1048576) {await reader.cancel(); throw new Refusal("INVALID_RESPONSE");}
chunks.push(next.value);
}
raw = Buffer.concat(chunks);
} catch (error) {if (error instanceof Refusal) throw error; throw new Unreachable();}
let value: ObjectValue;
try {value = object(decode(raw));} catch {throw new Refusal("INVALID_RESPONSE");}
const requestId = typeof value.request_id === "string" && /^req_[A-Za-z0-9_-]{1,120}$/.test(value.request_id) ? value.request_id : null;
if (!response.ok || value.success !== true) {
const error = value.error && typeof value.error === "object" ? object(value.error) : {};
const code = typeof error.code === "string" && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : "HTTP_ERROR";
throw new Refusal(code, requestId);
}
return {body: value, requestId};
}
private verify(envelope: string, source: string, checkpoint: string | null, requestId: string | null): Decision {
if (!verifyEnvelope(this.trusted, Buffer.from(envelope))) throw new Refusal("SIGNATURE_INVALID", requestId);
// Only after byte-level verification, decode a semantic view for policy.
const payload = object(object(JSON.parse(envelope)).payload), now = Date.now();
if (payload.product_id !== this.app || payload.environment !== "sandbox" ||
object(payload.device).installation_hash !== createHash("sha256").update(this.installation).digest("hex")) throw new Refusal("BINDING_MISMATCH", requestId);
const server = time(payload.server_time), next = time(payload.next_validation_at), grace = time(payload.offline_grace_until);
if (next < server || grace < server) throw new Refusal("INVALID_TIME", requestId);
const previous = checkpoint === null ? server : Math.max(server, time(checkpoint));
if (now + 300000 < previous) throw new Refusal("CLOCK_ROLLBACK_DETECTED", requestId);
if (payload.status !== "active") throw new Refusal("LICENSE_NOT_ACTIVE", requestId);
if (payload.starts_at !== null && payload.starts_at !== undefined && now < time(payload.starts_at)) throw new Refusal("LICENSE_NOT_STARTED", requestId);
if (payload.expires_at !== null && now >= time(payload.expires_at)) throw new Refusal("LICENSE_EXPIRED", requestId);
if (source !== "online" && now > grace) throw new Refusal("OFFLINE_GRACE_EXPIRED", requestId);
const features = object(payload.features), seat = features.seat_count;
return {valid: true, source, signature_valid: true, error_code: null, request_id: requestId,
pdf_export_allowed: features.pdf_export === true, seat_count: typeof seat === "number" && Number.isSafeInteger(seat) && seat >= 0 ? seat : null,
message: "Your license is ready."};
}
private accepted(state: State, source: string, requestId: string | null): Decision {
if (!state.envelope) throw new Refusal("OFFLINE_CACHE_MISSING");
const result = this.verify(state.envelope, source, state.checkpoint, requestId);
this.write("session.json", {...state, checkpoint: new Date().toISOString()});
return result;
}
async run(action: string): Promise {
let state = this.state();
if (action === "status") return {activated: state !== null, installation_hash: createHash("sha256").update(this.installation).digest("hex"), storage_security: "permission-protected-file"};
if (action === "activate") {
const license = process.env.KDP_LICENSE_KEY ?? "", email = process.env.KDP_CUSTOMER_EMAIL ?? "";
if (!license.startsWith("TEST") || /[\r\n]/.test(license + email)) throw new Error("Use a Sandbox test key and valid input.");
// Never replace an existing installation token implicitly or retry a
// mutation automatically. Reconcile an uncertain activation first.
if (state) throw new Refusal("ALREADY_ACTIVATED");
const response = await this.request("/licenses/activate", {license_key: license, device_id: this.installation,
device_name: "TypeScript REST example", platform: process.platform, app_version: "1.0.0", ...(email ? {email} : {})});
const token = response.body.activation_token;
if (typeof token !== "string" || !/^act_[A-Za-z0-9_-]+$/.test(token)) throw new Refusal("INVALID_RESPONSE", response.requestId);
try {
const envelope = canonical(object(response.body.license));
const result = this.verify(envelope, "online", null, response.requestId);
this.write("session.json", {token, envelope, checkpoint: new Date().toISOString()});
return result;
} catch (error) {
try {await this.request("/licenses/deactivate", {}, token);} catch { /* Reconcile uncertain release with the developer. */ }
throw error;
}
}
if (!state) throw new Refusal("NOT_ACTIVATED");
if (action === "deactivate") {
const response = await this.request("/licenses/deactivate", {}, state.token);
if (response.body.deactivated !== true) throw new Refusal("INVALID_RESPONSE", response.requestId);
this.clear();
return {valid: true, source: "online", signature_valid: false, error_code: null, request_id: response.requestId,
pdf_export_allowed: false, seat_count: null, message: "This installation has been deactivated."};
}
if (action === "check" && state.envelope) {
try {
const checked = this.verify(state.envelope, "cache", state.checkpoint, null);
const payload = object(object(JSON.parse(state.envelope)).payload);
if (Date.now() < time(payload.next_validation_at)) {this.write("session.json", {...state, checkpoint: new Date().toISOString()}); return checked;}
} catch {state = {...state, envelope: null}; this.write("session.json", state);}
}
try {
const response = await this.request("/licenses/validate", {}, state.token);
const envelope = canonical(object(response.body.license));
const result = this.verify(envelope, "online", state.checkpoint, response.requestId);
this.write("session.json", {...state, envelope, checkpoint: new Date().toISOString()}); return result;
} catch (error) {
if (error instanceof Unreachable) return this.accepted(state, "offline", null);
if (error instanceof Refusal) {
if (["INVALID_TOKEN", "DEVICE_MISMATCH", "LICENSE_REVOKED", "LICENSE_EXPIRED", "LICENSE_CANCELLED"].includes(error.code)) this.clear();
else this.write("session.json", {...state, envelope: null});
}
throw error;
}
}
}
async function main(): Promise {
const action = process.argv[2] ?? "";
if (process.argv.length !== 3 || !["status", "activate", "check", "validate", "deactivate"].includes(action)) throw new Error("Choose status, activate, check, validate or deactivate.");
const session = new Session();
try {
try {
const result = await session.run(action); console.log(JSON.stringify(result));
process.exitCode = "valid" in result && result.valid !== true ? 1 : 0;
} catch (error) {
if (error instanceof Refusal || error instanceof Unreachable) {
console.log(JSON.stringify(denied(error instanceof Refusal ? error.code : "NETWORK_ERROR", "local", error instanceof Refusal ? error.requestId : null)));
process.exitCode = 1;
} else throw error;
}
} finally {session.close();}
}
main().catch(() => {console.error("Check your application settings and private storage. Please contact your software provider if the problem continues."); process.exitCode = 2;});
```
Download the complete, already published [verification source archive](/docs/downloads/security-verifiers.zip) and copy security/javascript/canonical.mjs and security/javascript/verify.mjs into the layout above without modification. Both files are also displayed in full on the [JavaScript verification page](/docs/v1/security/verify-ed25519/javascript). This client imports those exact tested sources; it does not omit or replace verification logic.
The companion modules retain numeric tokens, reject duplicate properties, malformed UTF-8/Unicode, trailing content, excessive depth and oversized input. Signature verification independently derives the key ID and checks the key/envelope algorithm before Ed25519 verification.
## Configure the application
Set these values through trusted application configuration or your private development environment. Do not print the environment in logs or embed a [customer](/docs/v1/start/glossary#customer)'s license in distributed source.
| Variable | Required | Meaning |
| --- | --- | --- |
| KDP_APP_ID | Yes | Public App ID, not internal [product](/docs/v1/start/glossary#product) ID or [developer API key](/docs/v1/start/glossary#developer-api-key) |
| KDP_BASE_URL | No | API URL, default https://licensing.kdpika.com/api/v1; unlike official SDK constructors, this REST example includes /api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; namespaces include API URL, App ID, SANDBOX and this client's format |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to the independently trusted public-key JSON document |
| KDP_LICENSE_KEY | Activation only | Synthetic Sandbox key starting TEST for this example's safety gate |
| KDP_CUSTOMER_EMAIL | Optional | Matching license email; key-only activation is an actual API mode, not an email policy enforced by the example |
| KDP_ALLOW_LOOPBACK | Local testing only | Set 1 to allow explicit localhost/127.0.0.1 HTTP; never an arbitrary non-TLS host |
Environment is deliberately fixed to SANDBOX. [Live](/docs/v1/start/glossary#live) keys, URL credentials, query strings, fragments and nonlocal HTTP are blocked. Redirects are not followed, and the request timeout is 15 seconds. Requests and response bodies are not logged.
## Run the lifecycle
Run the compiled client with exactly one action: status, activate, check, validate or deactivate. Keep the same state root and App ID between invocations.
1. Run status. Expected: activated=false and an installation hash, never the stored token. This action is not a validity check.
2. Set the Sandbox key and matching email, then run activate. Expected: a signed accepted decision with signature_valid=true or a friendly refusal. An existing token is never replaced implicitly.
3. Run check. Expected: eligible verified cache before next_validation_at, otherwise an online validation attempt. No valid result means no feature access.
4. Run validate. Expected: an online attempt; a transport failure can use only a still-valid signed offline cache. This is not an unconditional network-only guarantee.
5. Run deactivate. Expected: server-confirmed seat release and removal of the local session. Its valid=true describes the release operation; signature_valid=false and pdf_export_allowed=false ensure it cannot be mistaken for access.
6. Run status again. Expected: activated=false and the same installation hash. Deactivation does not reset cumulative activation_count.
Exit codes are 0 for an accepted operation/status, 1 for a licensing or connectivity refusal, and 2 for invalid configuration, unsafe local state or an unexpected failure. Output contains decisions and a safe [request ID](/docs/v1/start/glossary#request-id), not raw signed customer data, keys or tokens.
## Contract and feature policy
The client calls the actual activate, validate and deactivate endpoints. Activation sends X-App-ID and X-Environment: SANDBOX; validation sends Bearer act_ and X-Installation-ID; the installation token is never an lmp_ developer management key. See [authentication](/docs/v1/start/authentication).
Signature verification uses a lossless parsed representation to recover the [signed envelope](/docs/v1/start/glossary#signed-envelope) without changing numeric tokens. Only after authentication does the client decode a semantic view for policy. It requires the actual App ID, environment=sandbox, SHA-256 installation binding and active status. Start, expiry and signed validation/grace dates are checked separately.
pdf_export is enabled only when a fully accepted result contains the boolean true. seat_count is exposed only as a nonnegative safe integer; absent, fractional, unsafe or nonnumeric limits remain null. These are example feature names: configure the features your own application actually uses. Constructing an object with valid=true is never a verification step.
## Offline grace and clock checks
The signed server_time, next_validation_at and offline_grace_until are mandatory valid timestamps in this example. The validation/grace dates cannot precede server_time. This strict client policy is deliberately fail-closed; it is not a new API response contract or a change to existing SDK behavior.
Before next_validation_at, check may use eligible verified cache. Afterwards, a transport failure may use a still-valid signed cache. An HTTP rejection, malformed success, signature failure or binding failure is not permission to enter offline mode. Explicit refusal removes the cached envelope; selected invalid-token/[device](/docs/v1/start/glossary#device)/revocation/expiry/cancellation codes also remove the token.
The current instant may not be more than 300 seconds behind the later signed server time or stored checkpoint. Expiry rejects at the exact expiry instant. Grace accepts its exact boundary and rejects after it. Grace is not extended by adding it to next_validation_at. The test harness evaluates the actual signed boundaries through a test-only clock preload; the distributed client exposes no clock override variable.
No offline scheme prevents an attacker who controls and patches the local application or restores a full machine snapshot. Preserve the signature/time checks and explain connectivity requirements to users without promising absolute anti-tamper protection.
## Storage, concurrency and token protection
The sample creates a persistent random [installation ID](/docs/v1/start/glossary#installation-id), a private namespace and an atomic permission-protected session file. The session contains the token, raw signed envelope and last accepted checkpoint. It rejects symlink files, oversized state/key files and invalid identity/session formats. It never resets an invalid installation identity automatically.
An exclusive lock serializes operations for the namespace. If a process crashes, reconcile the process state before removing its exact stale lock; do not run multiple writers or automatically discard a lock that may still be in use. Atomic writes and Unix permission modes are not encrypted vault storage or protection against a malicious same-user process. Windows ACLs must be reviewed on the real target system. See [secure token storage](/docs/v1/security/secure-token-storage) before production desktop distribution.
Do not share one namespace across unrelated buyers, tenants, products or environments. Store state outside a public server directory. A backend route wrapping this client still needs your application's authentication, tenant authorization and CSRF protection. An Electron renderer must not receive the token, raw cache, private paths or an arbitrary SDK/REST invocation channel.
## Errors and uncertain operations
The API's error.code and request_id are handled through bounded safe strings. Unknown errors fail closed. See the [complete error table](/docs/v1/api/errors) for server meanings. Example-local errors such as ALREADY_ACTIVATED, BINDING_MISMATCH, INVALID_TIME and SIGNATURE_INVALID describe this client policy; they are not invented server emissions.
No mutation is automatically retried. After a lost activation response, reconcile whether the server already consumed an activation before retrying. If a token was returned but signature/policy acceptance fails, the example attempts a best-effort release of that exact new token; a failed release still needs reconciliation by the developer. A missing/incomplete response cannot guarantee that no server mutation happened.
Keep friendly messages separate from support diagnostics. Record only time, action, application version, safe error code and safe request ID. Do not log Authorization headers, environment dumps, [license keys](/docs/v1/start/glossary#license-key), tokens, raw exceptions, private paths or customer-bearing payloads.
## Packaging and integration notes
Ship the compiled client with the unchanged verification modules and a controlled trusted-key configuration. Do not include a test-only clock preload, populated state directory, customer license key or developer API key. Re-run real signed response, cache reload, explicit refusal, offline and deactivation tests after bundling.
This sample is a trusted Node/CLI/Electron-main integration. It is not suitable for ordinary browser storage or unrestricted renderer access. Native Windows/Linux execution, encrypted vault adapters and a standalone bundled binary are not certified by these macOS source-client checks. API v1 is independent of this sample's version label and the four official SDK package versions.
## Verification recorded so far
Strict compilation uses the pinned TypeScript and Node type versions. The exact program runs against a fresh isolated local Sandbox with real signed responses. Tests cover matching/wrong email, stable identity, cache with zero HTTP, online validation, injected transport loss, modified cache, rollback, explicit HTTP refusal, modified server signature, redirect rejection, deactivation and unsafe setup inputs. The real signed grace and expiry boundaries are checked without editing the signed data.
All23 published signature vectors also pass through the TypeScript verification modules. The complete project builds from a fresh exact source archive with its pinned lockfile. Signature-vector results establish authenticity and canonicalization, not present license validity. The separate lifecycle tests enforce binding, status, clock and feature policy. Native target-OS packaging and vault behavior need their own release tests.
## Expected result
Your application preserves one installation identity, authenticates signed state and applies policy before any feature access. Explicit refusals remain refusals, and a successful release never unlocks the application.
## Common mistakes
- Copying this REST API base URL into an official SDK constructor, which expects only the site origin.
- Using JSON.parse/JSON.stringify as the signed-byte canonicalizer and losing 19.0 to 19.
- Trusting a public key received from an untrusted envelope instead of your controlled key configuration.
- Treating token presence, HTTP 200, a true feature helper or deactivation success as current permission.
- Deleting identity/state to fix a refusal and unknowingly consuming another activation.
## Security notes
This complete example uses the existing server contract without changing licensing behavior. Keep HTTPS, trusted-key verification, product/environment/installation binding, expiry/grace and clock checks. Test-mode guards reduce mistakes; they do not replace the platform's credential and environment validation. No local constructed or synthetic test result authorizes a real customer installation.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Configuration exit 2 | Missing absolute path, unsafe URL, invalid file or busy/stale lock | Correct trusted configuration; inspect the exact private namespace without exposing it to users |
| INVALID_LICENSE | Key, email, App ID or environment mismatch | Use the real Sandbox values together; do not retry unchanged input repeatedly |
| SIGNATURE_INVALID | Modified data, wrong trusted key or incompatible bytes | Deny access; compare the published vectors and controlled key version |
| OFFLINE_CACHE_MISSING | No accepted signed cache remains | Reconnect and validate; do not manufacture an offline result |
| CLOCK_ROLLBACK_DETECTED | Device clock is behind trusted time | Correct the system clock and reconnect |
| Release failed | Network/server refusal or uncertain response | Keep the installation state and reconcile with the software provider |
## Next steps
Review [signed responses](/docs/v1/security/signed-responses), [offline mode](/docs/v1/security/offline-mode) and the [API conventions](/docs/v1/api/conventions) before adapting this Sandbox client.
## Related
- [Public key](/docs/v1/api/public-key)
- [Activation](/docs/v1/api/activate)
- [Deactivation](/docs/v1/api/deactivate)
- [Lossless verification source](/docs/v1/security/verify-ed25519/javascript)
# Security reference
Canonical: https://licensing.kdpika.com/docs/v1/security
---
slug: "/docs/v1/security"
title: "Security reference"
description: "Understand signed decisions, private storage, installation identity and safe failure behavior."
section: "Security"
order: 52
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "hub"
---
## What you can do here
Understand signed decisions, private storage, installation identity and safe failure behavior.
- [Signed responses](/docs/v1/security/signed-responses) — Understand the exact Ed25519 envelope, PHP canonical bytes, trusted keys, time checks and replay limits before accepting a [license](/docs/v1/start/glossary#license).
- [Verify Ed25519 signatures](/docs/v1/security/verify-ed25519) — Run independent signature verifiers in fourteen languages with the same lossless JSON contract and shared conformance tests.
- [Signature test vectors](/docs/v1/security/test-vectors) — Download real public-safe Signer vectors and test authenticity separately from expiry, trust and license policy.
- [Offline mode and grace](/docs/v1/security/offline-mode) — Configure real SDK cache and grace behavior, test connection failures safely and understand revocation and clock limitations.
- [Entitlements and features](/docs/v1/security/entitlements-and-features) — Define product-plan features, enforce boolean and numeric entitlements safely and understand cached plan changes.
- [Threat model](/docs/v1/security/threat-model) — Understand licensing trust boundaries, attacker capabilities, implemented controls and limits on customer-controlled machines.
- [Application hardening](/docs/v1/security/hardening) — Combine realistic code signing, short validation policies, server authorization and tamper-evident operational records.
- [Secure token storage](/docs/v1/security/secure-token-storage) — Choose native token storage by operating system and language, with explicit SDK fallbacks and tested helper examples.
- [Installation identity](/docs/v1/security/installation-id) — Generate a random stable [installation ID](/docs/v1/start/glossary#installation-id) without hardware fingerprints and recover safely after reinstall or state loss.
- [API key security and leak response](/docs/v1/security/api-key-leak-response) — [Scope](/docs/v1/start/glossary#scope) management credentials and respond to a leak with immediate revocation, request-ID investigation and controlled recovery.
- [Webhook security](/docs/v1/security/webhook-security) — Verify exact raw HMAC bytes, reject stale or duplicate events and coordinate endpoint-secret rotation safely.
- [Developer data protection responsibilities](/docs/v1/security/developer-data-protection) — Understand actual [customer](/docs/v1/start/glossary#customer) data, export and retention controls without mistaking pseudonymisation or account closure for complete erasure.
- [Security and responsible disclosure](/security) — Review [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing)'s implemented controls, known limits and safe vulnerability-reporting contact.
## Next steps
[Follow the Complete Integration Guide](/docs/v1/integration-guide)
# Java REST client
Canonical: https://licensing.kdpika.com/docs/v1/sdks/java
---
title: "Java REST client"
description: "Build a complete Java Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating."
slug: "/docs/v1/sdks/java"
section: "SDKs and languages"
order: 52
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/activate", "/docs/v1/security/offline-mode"]
draft: false
---
Use the real API v1 from a trusted Java process with explicit signature and policy checks before enabling [features](/docs/v1/start/glossary#feature). This REST sample is not an additional [official SDK](/docs/v1/start/glossary#official-sdk).
## What you will learn
- Activate, validate and deactivate one stable [Sandbox](/docs/v1/start/glossary#sandbox) installation.
- Preserve signed bytes and verify the trusted Ed25519 key before policy.
- Handle cache, [offline grace](/docs/v1/start/glossary#offline-grace), explicit rejection and safe user messages.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [signed responses](/docs/v1/security/signed-responses).
- Have the [product](/docs/v1/start/glossary#product) App ID, a Sandbox [license](/docs/v1/start/glossary#license) and its matching [customer](/docs/v1/start/glossary#customer) email when supplied.
- Obtain the actual [public-key document](/docs/v1/api/public-key) over independently trusted HTTPS and distribute/pin it through controlled application configuration, not from an untrusted envelope.
## Runtime and application type
Java17+, Gson2.14.0, Bouncy Castle1.86. JVM desktop, CLI and backend processes. The complete session core uses Java HttpClient and Java NIO file locking. Keep the two pinned jars at runtime. Windows classpaths use semicolons; the POSIX shell build needs a compatible development shell or equivalent IDE configuration.
## Complete project and integrity
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
[kdpika-java-rest-example-1.0.0.zip](/docs/downloads/kdpika-java-rest-example-1.0.0.zip) · 9925 bytes · version 1.0.0
SHA-256: 75a1647186ff9642c3d48c7e92772f8dcae251406fb7dd439cad453bdf969aa3
Extract the entire project, preserving its samples/sdks and samples/security layout. The archive includes all first-party client/verifier sources and pinned manifests/locks; dependency downloads are development-time restoration, never extra services needed by the PHP host. Do not flatten the tree or replace the verification helper with a generic JSON reserializer.
## Install and build
1. Verify the archive SHA-256 against the current table. Expected: exact digest match; otherwise stop.
2. Extract into a clean private development directory. Expected: complete samples tree with the language project.
3. Enter samples/sdks/java. Expected: the main client and build/dependency files are present.
4. Install the stated runtime/native libraries, then run the full build command below. Expected: successful syntax check or native executable, not a registry SDK download.
```bash
sh build.sh
```
The source scripts compile the exact files and validate third-party download checksums where applicable. Compiler toolchains are development dependencies, not tools to install on KDPika shared hosting. macOS is the actual tested runtime; other targets require their own build/permission/TLS-root checks.
## Application configuration
| Variable | Required | Meaning |
| --- | --- | --- |
| KDP_APP_ID | Yes | [Public App ID](/docs/v1/start/glossary#public-app-id), not internal product ID or [developer](/docs/v1/start/glossary#developer) key |
| KDP_BASE_URL | No | HTTPS API base ending /api/v1; default https://licensing.kdpika.com/api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; keep it stable |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to an independently trusted public-key document |
| KDP_LICENSE_KEY | [Activation](/docs/v1/start/glossary#activation) | Sandbox key beginning TEST; never embedded in distributed source |
| KDP_CUSTOMER_EMAIL | Optional | Actual matching customer email for email-bound activation |
| KDP_ALLOW_LOOPBACK | Local testing only | 1 permits explicit localhost/127.0.0.1 HTTP, not arbitrary HTTP hosts |
Environment is fixed to SANDBOX in this example. Unsafe URL credentials/query/fragment and nonlocal HTTP are rejected. Official SDK constructors expect the site origin; these REST programs expect /api/v1. Store state and trusted keys outside public web roots. Do not ship lmp_ developer management keys.
## Complete runnable client
```java
// Complete Sandbox REST session, Java17+, Gson2.14.0, BC1.86.
// Compile with the unchanged P5 Canonical.java/Verify.java and pinned jars.
// Run Session status|activate|check|validate|deactivate; safe JSON decisions only.
import com.google.gson.*;
import java.net.URI;
import java.net.http.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.time.*;
import java.util.*;
public final class Session implements AutoCloseable {
static final Gson JSON=new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
static final int MAX=1048576;
static final class Denied extends Exception {
private static final long serialVersionUID=1L;
final String code,requestId;
Denied(String code){this(code,null);} Denied(String code,String requestId){super(code);this.code=code;this.requestId=requestId;}
}
static final class Offline extends Exception {private static final long serialVersionUID=1L;}
final String app,base,installation;
final Path directory;
final byte[] trusted;
final FileChannel channel;
final FileLock lock;
final HttpClient http=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
static String config(String name){String value=System.getenv(name);if(value==null||value.isBlank()||value.contains("\r")||value.contains("\n"))throw new IllegalArgumentException("Missing configuration");return value;}
static String digest(String text)throws Exception{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(text.getBytes(StandardCharsets.UTF_8)));}
static void permission(Path path,String mode)throws Exception {if(Files.getFileStore(path).supportsFileAttributeView("posix"))Files.setPosixFilePermissions(path,PosixFilePermissions.fromString(mode));}
static byte[] read(Path file)throws Exception {if(Files.isSymbolicLink(file)||!Files.isRegularFile(file,LinkOption.NOFOLLOW_LINKS)||Files.size(file)>MAX)throw new IllegalArgumentException("Unsafe state file");return Files.readAllBytes(file);}
static JsonObject parsed(byte[] bytes)throws Exception {Object tree=Canonical.parse(bytes);if(!(tree instanceof Map))throw new Denied("INVALID_RESPONSE");return JsonParser.parseString(Canonical.encode(tree)).getAsJsonObject();}
static String string(JsonObject object,String name)throws Denied {var value=object.get(name);if(value==null||!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isString())throw new Denied("INVALID_RESPONSE");return value.getAsString();}
static Instant instant(String value)throws Denied {try{return OffsetDateTime.parse(value).toInstant();}catch(Exception error){throw new Denied("INVALID_TIME");}}
Session()throws Exception {
app=config("KDP_APP_ID");if(!app.matches("[A-Za-z0-9_-]{3,160}"))throw new IllegalArgumentException("App ID");
var uri=URI.create(System.getenv().getOrDefault("KDP_BASE_URL","https://licensing.kdpika.com/api/v1").replaceAll("/+$",""));
boolean local="http".equals(uri.getScheme())&&Set.of("127.0.0.1","localhost").contains(uri.getHost())&&"1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if(uri.getHost()==null||(!"https".equals(uri.getScheme())&&!local)||uri.getUserInfo()!=null||uri.getQuery()!=null||uri.getFragment()!=null||!"/api/v1".equals(uri.getPath()))throw new IllegalArgumentException("Unsafe URL");
base=uri.toString();var root=Path.of(config("KDP_STATE_DIR"));var key=Path.of(config("KDP_TRUSTED_KEY_FILE"));
if(!root.isAbsolute()||!key.isAbsolute())throw new IllegalArgumentException("Absolute paths required");trusted=read(key);
var publicKey=parsed(trusted);if(!"Ed25519".equals(string(publicKey,"algorithm"))||!"base64url".equals(string(publicKey,"encoding")))throw new IllegalArgumentException("Trusted key");
Files.createDirectories(root);if(Files.isSymbolicLink(root))throw new IllegalArgumentException("Root symlink");
directory=root.resolve(digest(base+"|"+app+"|SANDBOX|java-rest-v1").substring(0,20));Files.createDirectories(directory);
if(Files.isSymbolicLink(directory))throw new IllegalArgumentException("Namespace symlink");permission(directory,"rwx------");
channel=FileChannel.open(directory.resolve(".lock"),StandardOpenOption.CREATE,StandardOpenOption.WRITE,LinkOption.NOFOLLOW_LINKS);permission(directory.resolve(".lock"),"rw-------");
FileLock acquired=channel.tryLock();if(acquired==null){channel.close();throw new IllegalStateException("Busy installation");}lock=acquired;
try {
var identity=directory.resolve("installation.json");
if(!Files.exists(identity,LinkOption.NOFOLLOW_LINKS)){var value=new JsonObject();value.addProperty("installation_id","lmp-install-"+UUID.randomUUID());write("installation.json",value);}
installation=string(parsed(read(identity)),"installation_id");
if(!installation.matches("lmp-install-[a-f0-9-]{36}"))throw new IllegalArgumentException("Invalid identity; do not reset automatically");
}catch(Exception error){close();throw error;}
}
void write(String name,JsonObject value)throws Exception {
var target=directory.resolve(name);var temporary=Files.createTempFile(directory,"write-",".tmp");permission(temporary,"rw-------");
try(var file=FileChannel.open(temporary,StandardOpenOption.WRITE)){var bytes=ByteBuffer.wrap(JSON.toJson(value).getBytes(StandardCharsets.UTF_8));while(bytes.hasRemaining())file.write(bytes);file.force(true);}
try{Files.move(temporary,target,StandardCopyOption.ATOMIC_MOVE,StandardCopyOption.REPLACE_EXISTING);}
catch(AtomicMoveNotSupportedException error){Files.move(temporary,target,StandardCopyOption.REPLACE_EXISTING);}
}
JsonObject state()throws Exception {
var file=directory.resolve("session.json");if(!Files.exists(file,LinkOption.NOFOLLOW_LINKS))return null;
var value=parsed(read(file));if(!string(value,"token").matches("act_[A-Za-z0-9_-]+"))throw new IllegalArgumentException("Invalid token file");
instant(string(value,"checkpoint"));if(!value.has("envelope")||(!value.get("envelope").isJsonNull()&&!value.get("envelope").isJsonPrimitive()))throw new IllegalArgumentException("Invalid cache");return value;
}
void clear()throws Exception {Files.deleteIfExists(directory.resolve("session.json"));}
record Reply(JsonObject body,Object tree,String requestId) {}
Reply request(String endpoint,JsonObject body,String token)throws Exception {
var builder=HttpRequest.newBuilder(URI.create(base+endpoint)).timeout(Duration.ofSeconds(15)).header("Accept","application/json").header("Content-Type","application/json").header("User-Agent","KDPika-REST-Java-example/1.0");
if(token==null)builder.header("X-App-ID",app).header("X-Environment","SANDBOX");else builder.header("Authorization","Bearer "+token).header("X-Installation-ID",installation);
HttpResponse response;byte[] raw;
try {
response=http.send(builder.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body),StandardCharsets.UTF_8)).build(),HttpResponse.BodyHandlers.ofInputStream());
try(var input=response.body()){raw=input.readNBytes(MAX+1);}
}catch(java.io.IOException error){throw new Offline();}
catch(InterruptedException error){Thread.currentThread().interrupt();throw new Offline();}
if(raw.length>MAX)throw new Denied("INVALID_RESPONSE");
Object tree;JsonObject value;try{tree=Canonical.parse(raw);value=parsed(raw);}catch(Exception error){throw new Denied("INVALID_RESPONSE");}
String id=value.has("request_id")&&value.get("request_id").isJsonPrimitive()?value.get("request_id").getAsString():null;
if(id!=null&&!id.matches("req_[A-Za-z0-9_-]{1,120}"))id=null;
if(response.statusCode()<200||response.statusCode()>=300||!new JsonPrimitive(true).equals(value.get("success"))){
String code="HTTP_ERROR";if(value.has("error")&&value.get("error").isJsonObject()){var error=value.getAsJsonObject("error");if(error.has("code")&&error.get("code").isJsonPrimitive()&&error.get("code").getAsString().matches("[A-Z0-9_]{1,64}"))code=error.get("code").getAsString();}throw new Denied(code,id);
}return new Reply(value,tree,id);
}
JsonObject decision(String envelope,String source,String checkpoint,String requestId)throws Exception {
if(!Verify.verifyEnvelope(trusted,envelope.getBytes(StandardCharsets.UTF_8)))throw new Denied("SIGNATURE_INVALID",requestId);
var payload=parsed(envelope.getBytes(StandardCharsets.UTF_8)).getAsJsonObject("payload");var now=Instant.now();
if(!app.equals(string(payload,"product_id"))||!"sandbox".equals(string(payload,"environment"))||!digest(installation).equals(string(payload.getAsJsonObject("device"),"installation_hash")))throw new Denied("BINDING_MISMATCH",requestId);
var server=instant(string(payload,"server_time"));var next=instant(string(payload,"next_validation_at"));var grace=instant(string(payload,"offline_grace_until"));
if(next.isBefore(server)||grace.isBefore(server))throw new Denied("INVALID_TIME",requestId);
var latest=checkpoint==null?server:instant(checkpoint);if(server.isAfter(latest))latest=server;
if(now.plusSeconds(300).isBefore(latest))throw new Denied("CLOCK_ROLLBACK_DETECTED",requestId);
if(!"active".equals(string(payload,"status")))throw new Denied("LICENSE_NOT_ACTIVE",requestId);
if(payload.has("starts_at")&&!payload.get("starts_at").isJsonNull()&&now.isBefore(instant(string(payload,"starts_at"))))throw new Denied("LICENSE_NOT_STARTED",requestId);
if(!payload.get("expires_at").isJsonNull()&&!now.isBefore(instant(string(payload,"expires_at"))))throw new Denied("LICENSE_EXPIRED",requestId);
if(!source.equals("online")&&now.isAfter(grace))throw new Denied("OFFLINE_GRACE_EXPIRED",requestId);
var features=payload.getAsJsonObject("features");var result=accepted(source,requestId);result.addProperty("signature_valid",true);
result.addProperty("pdf_export_allowed",new JsonPrimitive(true).equals(features.get("pdf_export")));
var seat=features.get("seat_count");if(seat!=null&&seat.isJsonPrimitive()&&seat.getAsJsonPrimitive().isNumber())try{long count=seat.getAsBigDecimal().longValueExact();if(count>=0&&count<=9007199254740991L)result.addProperty("seat_count",count);}catch(ArithmeticException ignored){}
return result;
}
static JsonObject accepted(String source,String id){var value=new JsonObject();value.addProperty("valid",true);value.addProperty("source",source);value.addProperty("signature_valid",false);value.add("error_code",JsonNull.INSTANCE);value.addProperty("request_id",id);value.addProperty("pdf_export_allowed",false);value.add("seat_count",JsonNull.INSTANCE);value.addProperty("message","Your license is ready.");return value;}
JsonObject run(String action)throws Exception {
var state=state();if(action.equals("status")){var value=new JsonObject();value.addProperty("activated",state!=null);value.addProperty("installation_hash",digest(installation));value.addProperty("storage_security","permission-protected-file");return value;}
if(action.equals("activate")){
String license=config("KDP_LICENSE_KEY");if(!license.startsWith("TEST"))throw new IllegalArgumentException("Sandbox key required");if(state!=null)throw new Denied("ALREADY_ACTIVATED");
var body=new JsonObject();body.addProperty("license_key",license);body.addProperty("device_id",installation);body.addProperty("device_name","Java REST example");body.addProperty("platform",System.getProperty("os.name"));body.addProperty("app_version","1.0.0");String email=System.getenv("KDP_CUSTOMER_EMAIL");if(email!=null&&!email.isBlank())body.addProperty("email",config("KDP_CUSTOMER_EMAIL"));
var reply=request("/licenses/activate",body,null);String token=string(reply.body,"activation_token");if(!token.matches("act_[A-Za-z0-9_-]+"))throw new Denied("INVALID_RESPONSE",reply.requestId);
try{String envelope=Canonical.encode(((Map,?>)reply.tree).get("license"));var result=decision(envelope,"online",null,reply.requestId);var saved=new JsonObject();saved.addProperty("token",token);saved.addProperty("envelope",envelope);saved.addProperty("checkpoint",Instant.now().toString());write("session.json",saved);return result;}
catch(Exception error){try{request("/licenses/deactivate",new JsonObject(),token);}catch(Exception ignored){}throw error;}
}
if(state==null)throw new Denied("NOT_ACTIVATED");String token=string(state,"token");
if(action.equals("deactivate")){var reply=request("/licenses/deactivate",new JsonObject(),token);if(!new JsonPrimitive(true).equals(reply.body.get("deactivated")))throw new Denied("INVALID_RESPONSE",reply.requestId);clear();var result=accepted("online",reply.requestId);result.addProperty("message","This installation has been deactivated.");return result;}
if(action.equals("check")&&!state.get("envelope").isJsonNull())try{String envelope=string(state,"envelope");var result=decision(envelope,"cache",string(state,"checkpoint"),null);var payload=parsed(envelope.getBytes(StandardCharsets.UTF_8)).getAsJsonObject("payload");if(Instant.now().isBefore(instant(string(payload,"next_validation_at")))){state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}}
catch(Exception error){state.add("envelope",JsonNull.INSTANCE);write("session.json",state);}
try{var reply=request("/licenses/validate",new JsonObject(),token);String envelope=Canonical.encode(((Map,?>)reply.tree).get("license"));var result=decision(envelope,"online",string(state,"checkpoint"),reply.requestId);state.addProperty("envelope",envelope);state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}
catch(Offline error){if(state.get("envelope").isJsonNull())throw new Denied("OFFLINE_CACHE_MISSING");var result=decision(string(state,"envelope"),"offline",string(state,"checkpoint"),null);state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}
catch(Denied error){if(Set.of("INVALID_TOKEN","DEVICE_MISMATCH","LICENSE_REVOKED","LICENSE_EXPIRED","LICENSE_CANCELLED").contains(error.code))clear();else{state.add("envelope",JsonNull.INSTANCE);write("session.json",state);}throw error;}
}
public void close()throws java.io.IOException {try{lock.release();}finally{channel.close();}}
public static void main(String[] args){int exit=2;try{
if(args.length!=1||!Set.of("status","activate","check","validate","deactivate").contains(args[0]))throw new IllegalArgumentException("Action required");
try(var session=new Session()){try{var result=session.run(args[0]);System.out.println(JSON.toJson(result));exit=0;}
catch(Denied error){var result=accepted("local",error.requestId);result.addProperty("valid",false);result.addProperty("error_code",error.code);result.addProperty("message",error.code.equals("INVALID_LICENSE")?"Check your license key and matching email.":"Your license could not be confirmed. Connect and try again, or contact your software provider.");System.out.println(JSON.toJson(result));exit=1;}
catch(Offline error){var result=accepted("local",null);result.addProperty("valid",false);result.addProperty("error_code","NETWORK_ERROR");result.addProperty("message","Connect to the internet and try again.");System.out.println(JSON.toJson(result));exit=1;}}
}catch(Exception error){System.err.println("Check application settings and private storage. Contact your software provider if the problem continues.");}System.exit(exit);}
}
```
The complete project also contains the exact verification helper. Its source and all positive/negative vectors are available in the [security verifier archive](/docs/downloads/security-verifiers.zip). Derived reusable files remove only the original standalone CLI entry point/imports that became unused; they do not change the signature algorithm or canonicalization.
## Run the lifecycle
Use java -cp "classes:deps/gson.jar:deps/bcprov.jar" Session followed by exactly one action. Preserve the same App ID, state root and origin across processes.
1. Run status. Expected: activated=false for a new namespace and only an installation hash/storage category. Token presence is not a license validity decision.
2. Set a Sandbox key and matching email, then run activate. Expected: valid=true and signature_valid=true, or a safe refusal. An existing token is not silently replaced.
3. Run check. Expected: eligible authenticated cache before next_validation_at, otherwise an online validation attempt.
4. Run validate. Expected: an online attempt; only transport failure can use an eligible verified offline cache. Explicit HTTP rejection never grants offline permission.
5. Run deactivate. Expected: server-confirmed release, token/cache removal and no feature grant. valid=true here means the release operation succeeded, not permission to run a protected feature.
6. Run status again. Expected: activated=false and the same installation hash. Cumulative activation_count is not reset.
Exit0 means accepted operation/status, exit1 means refusal or connectivity denial, exit2 means unsafe configuration/state or unexpected failure. Output is safe decision metadata: never raw keys, tokens, Authorization headers or signed customer payloads.
## API contract and signature verification
Activation sends X-App-ID and X-Environment: SANDBOX plus license_key/device_id and optional matching email. Validation uses Bearer act_ and X-Installation-ID. Deactivation releases that token. See the [endpoint reference](/docs/v1/api/conventions) for full field/response/error tables.
The response parser preserves numeric lexemes, rejects duplicate/invalid JSON through the shared tested verifier, and authenticates the exact envelope. Only a verified payload enters policy. Key ID is derived from the independently trusted raw public key; wrong algorithm/key/signature is rejected. Never repair a mismatch by stripping signed fields or using JSON.stringify-style numeric normalization.
## Binding, features and fail-closed policy
The client requires its actual product App ID, environment=sandbox, SHA-256 installation binding and active status. It checks starts_at when present, expiry when nonnull, signed validation/grace dates and the last accepted checkpoint. Invalid data, status or policy is a refusal even if the payload carries a true feature.
pdf_export is allowed only for the boolean true on a fully accepted result. seat_count is exposed only as a nonnegative integer bounded by the shared safe-integer policy; otherwise it stays null. These example feature names must match your own product configuration. Constructing a result or reading token presence never authenticates a license.
## Offline grace and time checks
Before next_validation_at, check can return a verified cache. After that instant it validates online. Only a transport failure may use the existing signed state within offline_grace_until and expires_at. An explicit HTTP refusal, bad signature, malformed success or binding mismatch cannot extend access. Selected token/revocation/[device](/docs/v1/start/glossary#device)/expiry/cancellation refusals clear the token; other refusals clear cached authorization.
The last accepted checkpoint and signed server_time bound rollback with300 seconds tolerance. Grace is based on its signed deadline, not added to next_validation_at. Validating online does not promise permission if time/status/binding rules fail. No constructor flag creates an extra offline extension.
These strict sample policies are not changes to existing SDK behavior. A person controlling the host can patch the program or restore full snapshots. Use server-side authorization for high-value online actions and do not promise absolute local anti-tamper protection.
## Private storage, concurrency and recovery
The [installation ID](/docs/v1/start/glossary#installation-id) is random, persistent and independent of MAC/serial/hardware fingerprints. A namespaced private session stores the [activation token](/docs/v1/start/glossary#activation-token), lossless [signed envelope](/docs/v1/start/glossary#signed-envelope) and checkpoint. Writes are atomic and permissions are owner-only where supported; this is not automatic encryption or an OS-vault certification.
The program serializes one namespace. Java/Kotlin, Dart, Rust and the POSIX native core use an OS lock on a stable file; Go/Ruby/Swift use exclusive lock-file ownership and require manual reconciliation after a crash. Never delete an active lock or regenerate identity to avoid a refusal. A new identity can consume another cumulative activation. Keep separate namespaces for unrelated buyers/tenants.
Review [secure token storage](/docs/v1/security/secure-token-storage) for native vault adapters. POSIX modes do not replace Windows ACL review. Do not put state under public_html, bundle a populated cache, or expose raw SDK/client methods to a renderer/browser.
## Errors and uncertain mutations
The client accepts only bounded safe error/request identifiers and shows plain user messages. Unknown errors fail closed. No automatic mutation retry is supplied. A lost activation response may already have consumed a server activation; reconcile before retrying. If a returned new token fails signature/policy acceptance, the client attempts best-effort release, which can itself remain uncertain.
Read [the complete server error table](/docs/v1/api/errors). Local example identifiers such as BINDING_MISMATCH, ALREADY_ACTIVATED, SIGNATURE_INVALID and INVALID_TIME are client policy results, not invented server emissions. Never show raw stack traces, private paths or customer payloads to end users.
## Packaging and framework integration
JVM desktop, CLI and backend processes. The complete session core uses Java HttpClient and Java NIO file locking. Keep the two pinned jars at runtime. Windows classpaths use semicolons; the POSIX shell build needs a compatible development shell or equivalent IDE configuration.
Keep stable state outside executable extraction/install directories. Include the exact verification modules and required native/runtime libraries; never embed a developer key or populated token/cache. A web route still needs application authentication, tenant authorization and CSRF protection. A desktop renderer gets narrow safe decisions only, not arbitrary file paths or token access.
Re-test real activation, cache reload, offline loss, explicit refusal and release after bundling or updating dependencies. macOS native source execution is not proof that every operating-system package, signing/notarization setup or framework UI has passed.
## Recorded verification and limits
The exact native runtime executes the real isolated Sandbox lifecycle and21 negative/positive checks: wrong email, stable cross-process identity, cache without HTTP, online validation, injected network loss, modified offline cache, rollback, explicit HTTP503, modified signed response, no-follow307, recovery, release and unsafe setup guards. Refusal/outage/redirect injections are an explicit loopback test forwarder, not a claimed production outage.
Build evidence records actual toolchain/dependency hashes. All ten fresh project archives build and the complete native clients execute real Sandbox lifecycles. All23 published signature vectors pass in every REST language. Signature authenticity is separate from current license authorization. Native Windows/Linux/mobile GUI execution is not inferred from this macOS run.
## Expected result
Your application authenticates signed state, applies binding/time/status policy and enables only verified features. A refusal or successful seat release cannot accidentally unlock a feature.
## Common mistakes
- Using the internal product ID instead of the Public App ID.
- Losing numeric tokens before verifying the signature.
- Treating a true feature or HTTP success as a validity proof.
- Sharing one installation cache between unrelated tenants or deleting identity on failure.
## Security notes
Keep HTTPS verification, independent key trust and fail-closed policy. This example does not change the platform contract, process payments or grant universal anti-tamper protection. Preserve real API/SDK boundaries and keep credentials off browser/renderer surfaces.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Exit2 at startup | Unsafe URL, missing private path, bad identity or busy lock | Correct trusted configuration; do not expose raw diagnostics to users |
| INVALID_LICENSE | Key/email/App ID/environment mismatch | Use the actual matching Sandbox values |
| SIGNATURE_INVALID | Wrong key, modified state or changed canonical bytes | Deny access and run the published vectors |
| OFFLINE_CACHE_MISSING | No accepted signed state remains | Reconnect and validate; never manufacture a cache |
| CLOCK_ROLLBACK_DETECTED | System time behind trusted time | Correct the device clock and reconnect |
| Release uncertain | Lost response or network/server refusal | Preserve identity and reconcile the seat with your software provider |
## Next steps
Review [offline mode](/docs/v1/security/offline-mode), [signed responses](/docs/v1/security/signed-responses) and [API errors](/docs/v1/api/errors).
## Related
- [Activate](/docs/v1/api/activate)
- [Validate](/docs/v1/api/validate)
- [Deactivate](/docs/v1/api/deactivate)
- [Signature test vectors](/docs/v1/security/test-vectors)
# Help and integration notes
Canonical: https://licensing.kdpika.com/docs/v1/help
---
slug: "/docs/v1/help"
title: "Help and integration notes"
description: "Find troubleshooting, compatibility disclosures and documentation tools without losing your integration path."
section: "Help and notes"
order: 53
audience: "all"
level: "beginner"
last_updated: "2026-10-21"
version: "v1"
related: []
draft: false
layout: "hub"
---
## What you can do here
Find troubleshooting, compatibility disclosures and documentation tools without losing your integration path.
- [Using the documentation](/docs/v1/documentation) — Read, search, navigate and export [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) documentation without an account.
- [Documentation components](/docs/v1/components) — Try the documentation's callouts, language tabs, code blocks, tables, diagrams and keyboard controls.
- [Current integration limitations](/docs/v1/integration-notes) — Read the [source-confirmed](/docs/v1/start/glossary#source-confirmed) SDK, signature, storage and retry limitations before integrating KDPika Licensing.
## Python SDK installation and source
Install [kdpika-licensing 1.1.5 from PyPI](https://pypi.org/project/kdpika-licensing/) using Python 3.11–3.14. The [Python SDK source on GitHub](https://github.com/kdpika/kdpika-licensing) is MIT-licensed. Read the [installation guide](/docs/v1/sdks/python) and [migration checklist](/docs/v1/sdks/upgrading); other language SDKs retain their documented distribution status.
## Next steps
[Follow the Complete Integration Guide](/docs/v1/integration-guide)
# Kotlin REST client
Canonical: https://licensing.kdpika.com/docs/v1/sdks/kotlin
---
title: "Kotlin REST client"
description: "Build a complete Kotlin Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating."
slug: "/docs/v1/sdks/kotlin"
section: "SDKs and languages"
order: 53
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/activate", "/docs/v1/security/offline-mode"]
draft: false
---
Use the real API v1 from a trusted Kotlin process with explicit signature and policy checks before enabling [features](/docs/v1/start/glossary#feature). This REST sample is not an additional [official SDK](/docs/v1/start/glossary#official-sdk).
## What you will learn
- Activate, validate and deactivate one stable [Sandbox](/docs/v1/start/glossary#sandbox) installation.
- Preserve signed bytes and verify the trusted Ed25519 key before policy.
- Handle cache, [offline grace](/docs/v1/start/glossary#offline-grace), explicit rejection and safe user messages.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [signed responses](/docs/v1/security/signed-responses).
- Have the [product](/docs/v1/start/glossary#product) App ID, a Sandbox [license](/docs/v1/start/glossary#license) and its matching [customer](/docs/v1/start/glossary#customer) email when supplied.
- Obtain the actual [public-key document](/docs/v1/api/public-key) over independently trusted HTTPS and distribute/pin it through controlled application configuration, not from an untrusted envelope.
## Runtime and application type
Kotlin2.4.21, JVM17+, Gson2.14.0, Bouncy Castle1.86. A real Kotlin/JVM entry point calls the complete adjacent Java session core through normal JVM interop. This shares the reviewed transport/storage/time/verification implementation, not a Python/Node subprocess. It is not a Kotlin Multiplatform or Android secure-storage implementation. Windows uses semicolon classpaths.
## Complete project and integrity
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
[kdpika-kotlin-rest-example-1.0.0.zip](/docs/downloads/kdpika-kotlin-rest-example-1.0.0.zip) · 10991 bytes · version 1.0.0
SHA-256: c3daa4dda79be936bf9979e27403d90eeaf2c08ce9eb5a4e2e4a818f20561403
Extract the entire project, preserving its samples/sdks and samples/security layout. The archive includes all first-party client/verifier sources and pinned manifests/locks; dependency downloads are development-time restoration, never extra services needed by the PHP host. Do not flatten the tree or replace the verification helper with a generic JSON reserializer.
## Install and build
1. Verify the archive SHA-256 against the current table. Expected: exact digest match; otherwise stop.
2. Extract into a clean private development directory. Expected: complete samples tree with the language project.
3. Enter samples/sdks/kotlin. Expected: the main client and build/dependency files are present.
4. Install the stated runtime/native libraries, then run the full build command below. Expected: successful syntax check or native executable, not a registry SDK download.
```bash
sh build.sh
```
The source scripts compile the exact files and validate third-party download checksums where applicable. Compiler toolchains are development dependencies, not tools to install on KDPika shared hosting. macOS is the actual tested runtime; other targets require their own build/permission/TLS-root checks.
## Application configuration
| Variable | Required | Meaning |
| --- | --- | --- |
| KDP_APP_ID | Yes | [Public App ID](/docs/v1/start/glossary#public-app-id), not internal product ID or [developer](/docs/v1/start/glossary#developer) key |
| KDP_BASE_URL | No | HTTPS API base ending /api/v1; default https://licensing.kdpika.com/api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; keep it stable |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to an independently trusted public-key document |
| KDP_LICENSE_KEY | [Activation](/docs/v1/start/glossary#activation) | Sandbox key beginning TEST; never embedded in distributed source |
| KDP_CUSTOMER_EMAIL | Optional | Actual matching customer email for email-bound activation |
| KDP_ALLOW_LOOPBACK | Local testing only | 1 permits explicit localhost/127.0.0.1 HTTP, not arbitrary HTTP hosts |
Environment is fixed to SANDBOX in this example. Unsafe URL credentials/query/fragment and nonlocal HTTP are rejected. Official SDK constructors expect the site origin; these REST programs expect /api/v1. Store state and trusted keys outside public web roots. Do not ship lmp_ developer management keys.
## Complete runnable client
```kotlin
// Kotlin/JVM REST client entry point; Kotlin2.4.21/JVM17+.
// Uses the complete adjacent Java session core, Gson2.14.0 and BC1.86.
// JVM interop shares one reviewed persistence/transport/signature policy.
// Run SessionKt status|activate|check|validate|deactivate; safe JSON only.
import com.google.gson.JsonObject
import kotlin.system.exitProcess
fun main(args: Array) {
var code = 2
try {
require(args.size == 1 && args[0] in setOf("status", "activate", "check", "validate", "deactivate"))
Session().use { client ->
try {
val result: JsonObject = client.run(args[0])
println(Session.JSON.toJson(result))
code = 0
} catch (error: Session.Denied) {
val result = Session.accepted("local", error.requestId)
result.addProperty("valid", false)
result.addProperty("error_code", error.code)
result.addProperty("message", if (error.code == "INVALID_LICENSE")
"Check your license key and matching email."
else "Your license could not be confirmed. Connect and try again, or contact your software provider.")
println(Session.JSON.toJson(result))
code = 1
} catch (error: Session.Offline) {
val result = Session.accepted("local", null)
result.addProperty("valid", false)
result.addProperty("error_code", "NETWORK_ERROR")
result.addProperty("message", "Connect to the internet and try again.")
println(Session.JSON.toJson(result))
code = 1
}
}
} catch (error: Exception) {
System.err.println("Check application settings and private storage. Contact your software provider if the problem continues.")
}
exitProcess(code)
}
```
```java
// Complete Sandbox REST session, Java17+, Gson2.14.0, BC1.86.
// Compile with the unchanged P5 Canonical.java/Verify.java and pinned jars.
// Run Session status|activate|check|validate|deactivate; safe JSON decisions only.
import com.google.gson.*;
import java.net.URI;
import java.net.http.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
import java.security.MessageDigest;
import java.time.*;
import java.util.*;
public final class Session implements AutoCloseable {
static final Gson JSON=new GsonBuilder().disableHtmlEscaping().serializeNulls().create();
static final int MAX=1048576;
static final class Denied extends Exception {
private static final long serialVersionUID=1L;
final String code,requestId;
Denied(String code){this(code,null);} Denied(String code,String requestId){super(code);this.code=code;this.requestId=requestId;}
}
static final class Offline extends Exception {private static final long serialVersionUID=1L;}
final String app,base,installation;
final Path directory;
final byte[] trusted;
final FileChannel channel;
final FileLock lock;
final HttpClient http=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).followRedirects(HttpClient.Redirect.NEVER).build();
static String config(String name){String value=System.getenv(name);if(value==null||value.isBlank()||value.contains("\r")||value.contains("\n"))throw new IllegalArgumentException("Missing configuration");return value;}
static String digest(String text)throws Exception{return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(text.getBytes(StandardCharsets.UTF_8)));}
static void permission(Path path,String mode)throws Exception {if(Files.getFileStore(path).supportsFileAttributeView("posix"))Files.setPosixFilePermissions(path,PosixFilePermissions.fromString(mode));}
static byte[] read(Path file)throws Exception {if(Files.isSymbolicLink(file)||!Files.isRegularFile(file,LinkOption.NOFOLLOW_LINKS)||Files.size(file)>MAX)throw new IllegalArgumentException("Unsafe state file");return Files.readAllBytes(file);}
static JsonObject parsed(byte[] bytes)throws Exception {Object tree=Canonical.parse(bytes);if(!(tree instanceof Map))throw new Denied("INVALID_RESPONSE");return JsonParser.parseString(Canonical.encode(tree)).getAsJsonObject();}
static String string(JsonObject object,String name)throws Denied {var value=object.get(name);if(value==null||!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isString())throw new Denied("INVALID_RESPONSE");return value.getAsString();}
static Instant instant(String value)throws Denied {try{return OffsetDateTime.parse(value).toInstant();}catch(Exception error){throw new Denied("INVALID_TIME");}}
Session()throws Exception {
app=config("KDP_APP_ID");if(!app.matches("[A-Za-z0-9_-]{3,160}"))throw new IllegalArgumentException("App ID");
var uri=URI.create(System.getenv().getOrDefault("KDP_BASE_URL","https://licensing.kdpika.com/api/v1").replaceAll("/+$",""));
boolean local="http".equals(uri.getScheme())&&Set.of("127.0.0.1","localhost").contains(uri.getHost())&&"1".equals(System.getenv("KDP_ALLOW_LOOPBACK"));
if(uri.getHost()==null||(!"https".equals(uri.getScheme())&&!local)||uri.getUserInfo()!=null||uri.getQuery()!=null||uri.getFragment()!=null||!"/api/v1".equals(uri.getPath()))throw new IllegalArgumentException("Unsafe URL");
base=uri.toString();var root=Path.of(config("KDP_STATE_DIR"));var key=Path.of(config("KDP_TRUSTED_KEY_FILE"));
if(!root.isAbsolute()||!key.isAbsolute())throw new IllegalArgumentException("Absolute paths required");trusted=read(key);
var publicKey=parsed(trusted);if(!"Ed25519".equals(string(publicKey,"algorithm"))||!"base64url".equals(string(publicKey,"encoding")))throw new IllegalArgumentException("Trusted key");
Files.createDirectories(root);if(Files.isSymbolicLink(root))throw new IllegalArgumentException("Root symlink");
directory=root.resolve(digest(base+"|"+app+"|SANDBOX|java-rest-v1").substring(0,20));Files.createDirectories(directory);
if(Files.isSymbolicLink(directory))throw new IllegalArgumentException("Namespace symlink");permission(directory,"rwx------");
channel=FileChannel.open(directory.resolve(".lock"),StandardOpenOption.CREATE,StandardOpenOption.WRITE,LinkOption.NOFOLLOW_LINKS);permission(directory.resolve(".lock"),"rw-------");
FileLock acquired=channel.tryLock();if(acquired==null){channel.close();throw new IllegalStateException("Busy installation");}lock=acquired;
try {
var identity=directory.resolve("installation.json");
if(!Files.exists(identity,LinkOption.NOFOLLOW_LINKS)){var value=new JsonObject();value.addProperty("installation_id","lmp-install-"+UUID.randomUUID());write("installation.json",value);}
installation=string(parsed(read(identity)),"installation_id");
if(!installation.matches("lmp-install-[a-f0-9-]{36}"))throw new IllegalArgumentException("Invalid identity; do not reset automatically");
}catch(Exception error){close();throw error;}
}
void write(String name,JsonObject value)throws Exception {
var target=directory.resolve(name);var temporary=Files.createTempFile(directory,"write-",".tmp");permission(temporary,"rw-------");
try(var file=FileChannel.open(temporary,StandardOpenOption.WRITE)){var bytes=ByteBuffer.wrap(JSON.toJson(value).getBytes(StandardCharsets.UTF_8));while(bytes.hasRemaining())file.write(bytes);file.force(true);}
try{Files.move(temporary,target,StandardCopyOption.ATOMIC_MOVE,StandardCopyOption.REPLACE_EXISTING);}
catch(AtomicMoveNotSupportedException error){Files.move(temporary,target,StandardCopyOption.REPLACE_EXISTING);}
}
JsonObject state()throws Exception {
var file=directory.resolve("session.json");if(!Files.exists(file,LinkOption.NOFOLLOW_LINKS))return null;
var value=parsed(read(file));if(!string(value,"token").matches("act_[A-Za-z0-9_-]+"))throw new IllegalArgumentException("Invalid token file");
instant(string(value,"checkpoint"));if(!value.has("envelope")||(!value.get("envelope").isJsonNull()&&!value.get("envelope").isJsonPrimitive()))throw new IllegalArgumentException("Invalid cache");return value;
}
void clear()throws Exception {Files.deleteIfExists(directory.resolve("session.json"));}
record Reply(JsonObject body,Object tree,String requestId) {}
Reply request(String endpoint,JsonObject body,String token)throws Exception {
var builder=HttpRequest.newBuilder(URI.create(base+endpoint)).timeout(Duration.ofSeconds(15)).header("Accept","application/json").header("Content-Type","application/json").header("User-Agent","KDPika-REST-Java-example/1.0");
if(token==null)builder.header("X-App-ID",app).header("X-Environment","SANDBOX");else builder.header("Authorization","Bearer "+token).header("X-Installation-ID",installation);
HttpResponse response;byte[] raw;
try {
response=http.send(builder.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body),StandardCharsets.UTF_8)).build(),HttpResponse.BodyHandlers.ofInputStream());
try(var input=response.body()){raw=input.readNBytes(MAX+1);}
}catch(java.io.IOException error){throw new Offline();}
catch(InterruptedException error){Thread.currentThread().interrupt();throw new Offline();}
if(raw.length>MAX)throw new Denied("INVALID_RESPONSE");
Object tree;JsonObject value;try{tree=Canonical.parse(raw);value=parsed(raw);}catch(Exception error){throw new Denied("INVALID_RESPONSE");}
String id=value.has("request_id")&&value.get("request_id").isJsonPrimitive()?value.get("request_id").getAsString():null;
if(id!=null&&!id.matches("req_[A-Za-z0-9_-]{1,120}"))id=null;
if(response.statusCode()<200||response.statusCode()>=300||!new JsonPrimitive(true).equals(value.get("success"))){
String code="HTTP_ERROR";if(value.has("error")&&value.get("error").isJsonObject()){var error=value.getAsJsonObject("error");if(error.has("code")&&error.get("code").isJsonPrimitive()&&error.get("code").getAsString().matches("[A-Z0-9_]{1,64}"))code=error.get("code").getAsString();}throw new Denied(code,id);
}return new Reply(value,tree,id);
}
JsonObject decision(String envelope,String source,String checkpoint,String requestId)throws Exception {
if(!Verify.verifyEnvelope(trusted,envelope.getBytes(StandardCharsets.UTF_8)))throw new Denied("SIGNATURE_INVALID",requestId);
var payload=parsed(envelope.getBytes(StandardCharsets.UTF_8)).getAsJsonObject("payload");var now=Instant.now();
if(!app.equals(string(payload,"product_id"))||!"sandbox".equals(string(payload,"environment"))||!digest(installation).equals(string(payload.getAsJsonObject("device"),"installation_hash")))throw new Denied("BINDING_MISMATCH",requestId);
var server=instant(string(payload,"server_time"));var next=instant(string(payload,"next_validation_at"));var grace=instant(string(payload,"offline_grace_until"));
if(next.isBefore(server)||grace.isBefore(server))throw new Denied("INVALID_TIME",requestId);
var latest=checkpoint==null?server:instant(checkpoint);if(server.isAfter(latest))latest=server;
if(now.plusSeconds(300).isBefore(latest))throw new Denied("CLOCK_ROLLBACK_DETECTED",requestId);
if(!"active".equals(string(payload,"status")))throw new Denied("LICENSE_NOT_ACTIVE",requestId);
if(payload.has("starts_at")&&!payload.get("starts_at").isJsonNull()&&now.isBefore(instant(string(payload,"starts_at"))))throw new Denied("LICENSE_NOT_STARTED",requestId);
if(!payload.get("expires_at").isJsonNull()&&!now.isBefore(instant(string(payload,"expires_at"))))throw new Denied("LICENSE_EXPIRED",requestId);
if(!source.equals("online")&&now.isAfter(grace))throw new Denied("OFFLINE_GRACE_EXPIRED",requestId);
var features=payload.getAsJsonObject("features");var result=accepted(source,requestId);result.addProperty("signature_valid",true);
result.addProperty("pdf_export_allowed",new JsonPrimitive(true).equals(features.get("pdf_export")));
var seat=features.get("seat_count");if(seat!=null&&seat.isJsonPrimitive()&&seat.getAsJsonPrimitive().isNumber())try{long count=seat.getAsBigDecimal().longValueExact();if(count>=0&&count<=9007199254740991L)result.addProperty("seat_count",count);}catch(ArithmeticException ignored){}
return result;
}
static JsonObject accepted(String source,String id){var value=new JsonObject();value.addProperty("valid",true);value.addProperty("source",source);value.addProperty("signature_valid",false);value.add("error_code",JsonNull.INSTANCE);value.addProperty("request_id",id);value.addProperty("pdf_export_allowed",false);value.add("seat_count",JsonNull.INSTANCE);value.addProperty("message","Your license is ready.");return value;}
JsonObject run(String action)throws Exception {
var state=state();if(action.equals("status")){var value=new JsonObject();value.addProperty("activated",state!=null);value.addProperty("installation_hash",digest(installation));value.addProperty("storage_security","permission-protected-file");return value;}
if(action.equals("activate")){
String license=config("KDP_LICENSE_KEY");if(!license.startsWith("TEST"))throw new IllegalArgumentException("Sandbox key required");if(state!=null)throw new Denied("ALREADY_ACTIVATED");
var body=new JsonObject();body.addProperty("license_key",license);body.addProperty("device_id",installation);body.addProperty("device_name","Java REST example");body.addProperty("platform",System.getProperty("os.name"));body.addProperty("app_version","1.0.0");String email=System.getenv("KDP_CUSTOMER_EMAIL");if(email!=null&&!email.isBlank())body.addProperty("email",config("KDP_CUSTOMER_EMAIL"));
var reply=request("/licenses/activate",body,null);String token=string(reply.body,"activation_token");if(!token.matches("act_[A-Za-z0-9_-]+"))throw new Denied("INVALID_RESPONSE",reply.requestId);
try{String envelope=Canonical.encode(((Map,?>)reply.tree).get("license"));var result=decision(envelope,"online",null,reply.requestId);var saved=new JsonObject();saved.addProperty("token",token);saved.addProperty("envelope",envelope);saved.addProperty("checkpoint",Instant.now().toString());write("session.json",saved);return result;}
catch(Exception error){try{request("/licenses/deactivate",new JsonObject(),token);}catch(Exception ignored){}throw error;}
}
if(state==null)throw new Denied("NOT_ACTIVATED");String token=string(state,"token");
if(action.equals("deactivate")){var reply=request("/licenses/deactivate",new JsonObject(),token);if(!new JsonPrimitive(true).equals(reply.body.get("deactivated")))throw new Denied("INVALID_RESPONSE",reply.requestId);clear();var result=accepted("online",reply.requestId);result.addProperty("message","This installation has been deactivated.");return result;}
if(action.equals("check")&&!state.get("envelope").isJsonNull())try{String envelope=string(state,"envelope");var result=decision(envelope,"cache",string(state,"checkpoint"),null);var payload=parsed(envelope.getBytes(StandardCharsets.UTF_8)).getAsJsonObject("payload");if(Instant.now().isBefore(instant(string(payload,"next_validation_at")))){state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}}
catch(Exception error){state.add("envelope",JsonNull.INSTANCE);write("session.json",state);}
try{var reply=request("/licenses/validate",new JsonObject(),token);String envelope=Canonical.encode(((Map,?>)reply.tree).get("license"));var result=decision(envelope,"online",string(state,"checkpoint"),reply.requestId);state.addProperty("envelope",envelope);state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}
catch(Offline error){if(state.get("envelope").isJsonNull())throw new Denied("OFFLINE_CACHE_MISSING");var result=decision(string(state,"envelope"),"offline",string(state,"checkpoint"),null);state.addProperty("checkpoint",Instant.now().toString());write("session.json",state);return result;}
catch(Denied error){if(Set.of("INVALID_TOKEN","DEVICE_MISMATCH","LICENSE_REVOKED","LICENSE_EXPIRED","LICENSE_CANCELLED").contains(error.code))clear();else{state.add("envelope",JsonNull.INSTANCE);write("session.json",state);}throw error;}
}
public void close()throws java.io.IOException {try{lock.release();}finally{channel.close();}}
public static void main(String[] args){int exit=2;try{
if(args.length!=1||!Set.of("status","activate","check","validate","deactivate").contains(args[0]))throw new IllegalArgumentException("Action required");
try(var session=new Session()){try{var result=session.run(args[0]);System.out.println(JSON.toJson(result));exit=0;}
catch(Denied error){var result=accepted("local",error.requestId);result.addProperty("valid",false);result.addProperty("error_code",error.code);result.addProperty("message",error.code.equals("INVALID_LICENSE")?"Check your license key and matching email.":"Your license could not be confirmed. Connect and try again, or contact your software provider.");System.out.println(JSON.toJson(result));exit=1;}
catch(Offline error){var result=accepted("local",null);result.addProperty("valid",false);result.addProperty("error_code","NETWORK_ERROR");result.addProperty("message","Connect to the internet and try again.");System.out.println(JSON.toJson(result));exit=1;}}
}catch(Exception error){System.err.println("Check application settings and private storage. Contact your software provider if the problem continues.");}System.exit(exit);}
}
```
The complete project also contains the exact verification helper. Its source and all positive/negative vectors are available in the [security verifier archive](/docs/downloads/security-verifiers.zip). Derived reusable files remove only the original standalone CLI entry point/imports that became unused; they do not change the signature algorithm or canonicalization.
## Run the lifecycle
Use java -cp "session.jar:classes:deps/gson.jar:deps/bcprov.jar" SessionKt followed by exactly one action. Preserve the same App ID, state root and origin across processes.
1. Run status. Expected: activated=false for a new namespace and only an installation hash/storage category. Token presence is not a license validity decision.
2. Set a Sandbox key and matching email, then run activate. Expected: valid=true and signature_valid=true, or a safe refusal. An existing token is not silently replaced.
3. Run check. Expected: eligible authenticated cache before next_validation_at, otherwise an online validation attempt.
4. Run validate. Expected: an online attempt; only transport failure can use an eligible verified offline cache. Explicit HTTP rejection never grants offline permission.
5. Run deactivate. Expected: server-confirmed release, token/cache removal and no feature grant. valid=true here means the release operation succeeded, not permission to run a protected feature.
6. Run status again. Expected: activated=false and the same installation hash. Cumulative activation_count is not reset.
Exit0 means accepted operation/status, exit1 means refusal or connectivity denial, exit2 means unsafe configuration/state or unexpected failure. Output is safe decision metadata: never raw keys, tokens, Authorization headers or signed customer payloads.
## API contract and signature verification
Activation sends X-App-ID and X-Environment: SANDBOX plus license_key/device_id and optional matching email. Validation uses Bearer act_ and X-Installation-ID. Deactivation releases that token. See the [endpoint reference](/docs/v1/api/conventions) for full field/response/error tables.
The response parser preserves numeric lexemes, rejects duplicate/invalid JSON through the shared tested verifier, and authenticates the exact envelope. Only a verified payload enters policy. Key ID is derived from the independently trusted raw public key; wrong algorithm/key/signature is rejected. Never repair a mismatch by stripping signed fields or using JSON.stringify-style numeric normalization.
## Binding, features and fail-closed policy
The client requires its actual product App ID, environment=sandbox, SHA-256 installation binding and active status. It checks starts_at when present, expiry when nonnull, signed validation/grace dates and the last accepted checkpoint. Invalid data, status or policy is a refusal even if the payload carries a true feature.
pdf_export is allowed only for the boolean true on a fully accepted result. seat_count is exposed only as a nonnegative integer bounded by the shared safe-integer policy; otherwise it stays null. These example feature names must match your own product configuration. Constructing a result or reading token presence never authenticates a license.
## Offline grace and time checks
Before next_validation_at, check can return a verified cache. After that instant it validates online. Only a transport failure may use the existing signed state within offline_grace_until and expires_at. An explicit HTTP refusal, bad signature, malformed success or binding mismatch cannot extend access. Selected token/revocation/[device](/docs/v1/start/glossary#device)/expiry/cancellation refusals clear the token; other refusals clear cached authorization.
The last accepted checkpoint and signed server_time bound rollback with300 seconds tolerance. Grace is based on its signed deadline, not added to next_validation_at. Validating online does not promise permission if time/status/binding rules fail. No constructor flag creates an extra offline extension.
These strict sample policies are not changes to existing SDK behavior. A person controlling the host can patch the program or restore full snapshots. Use server-side authorization for high-value online actions and do not promise absolute local anti-tamper protection.
## Private storage, concurrency and recovery
The [installation ID](/docs/v1/start/glossary#installation-id) is random, persistent and independent of MAC/serial/hardware fingerprints. A namespaced private session stores the [activation token](/docs/v1/start/glossary#activation-token), lossless [signed envelope](/docs/v1/start/glossary#signed-envelope) and checkpoint. Writes are atomic and permissions are owner-only where supported; this is not automatic encryption or an OS-vault certification.
The program serializes one namespace. Java/Kotlin, Dart, Rust and the POSIX native core use an OS lock on a stable file; Go/Ruby/Swift use exclusive lock-file ownership and require manual reconciliation after a crash. Never delete an active lock or regenerate identity to avoid a refusal. A new identity can consume another cumulative activation. Keep separate namespaces for unrelated buyers/tenants.
Review [secure token storage](/docs/v1/security/secure-token-storage) for native vault adapters. POSIX modes do not replace Windows ACL review. Do not put state under public_html, bundle a populated cache, or expose raw SDK/client methods to a renderer/browser.
## Errors and uncertain mutations
The client accepts only bounded safe error/request identifiers and shows plain user messages. Unknown errors fail closed. No automatic mutation retry is supplied. A lost activation response may already have consumed a server activation; reconcile before retrying. If a returned new token fails signature/policy acceptance, the client attempts best-effort release, which can itself remain uncertain.
Read [the complete server error table](/docs/v1/api/errors). Local example identifiers such as BINDING_MISMATCH, ALREADY_ACTIVATED, SIGNATURE_INVALID and INVALID_TIME are client policy results, not invented server emissions. Never show raw stack traces, private paths or customer payloads to end users.
## Packaging and framework integration
A real Kotlin/JVM entry point calls the complete adjacent Java session core through normal JVM interop. This shares the reviewed transport/storage/time/verification implementation, not a Python/Node subprocess. It is not a Kotlin Multiplatform or Android secure-storage implementation. Windows uses semicolon classpaths.
Keep stable state outside executable extraction/install directories. Include the exact verification modules and required native/runtime libraries; never embed a developer key or populated token/cache. A web route still needs application authentication, tenant authorization and CSRF protection. A desktop renderer gets narrow safe decisions only, not arbitrary file paths or token access.
Re-test real activation, cache reload, offline loss, explicit refusal and release after bundling or updating dependencies. macOS native source execution is not proof that every operating-system package, signing/notarization setup or framework UI has passed.
## Recorded verification and limits
The exact native runtime executes the real isolated Sandbox lifecycle and21 negative/positive checks: wrong email, stable cross-process identity, cache without HTTP, online validation, injected network loss, modified offline cache, rollback, explicit HTTP503, modified signed response, no-follow307, recovery, release and unsafe setup guards. Refusal/outage/redirect injections are an explicit loopback test forwarder, not a claimed production outage.
Build evidence records actual toolchain/dependency hashes. All ten fresh project archives build and the complete native clients execute real Sandbox lifecycles. All23 published signature vectors pass in every REST language. Signature authenticity is separate from current license authorization. Native Windows/Linux/mobile GUI execution is not inferred from this macOS run.
## Expected result
Your application authenticates signed state, applies binding/time/status policy and enables only verified features. A refusal or successful seat release cannot accidentally unlock a feature.
## Common mistakes
- Using the internal product ID instead of the Public App ID.
- Losing numeric tokens before verifying the signature.
- Treating a true feature or HTTP success as a validity proof.
- Sharing one installation cache between unrelated tenants or deleting identity on failure.
## Security notes
Keep HTTPS verification, independent key trust and fail-closed policy. This example does not change the platform contract, process payments or grant universal anti-tamper protection. Preserve real API/SDK boundaries and keep credentials off browser/renderer surfaces.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Exit2 at startup | Unsafe URL, missing private path, bad identity or busy lock | Correct trusted configuration; do not expose raw diagnostics to users |
| INVALID_LICENSE | Key/email/App ID/environment mismatch | Use the actual matching Sandbox values |
| SIGNATURE_INVALID | Wrong key, modified state or changed canonical bytes | Deny access and run the published vectors |
| OFFLINE_CACHE_MISSING | No accepted signed state remains | Reconnect and validate; never manufacture a cache |
| CLOCK_ROLLBACK_DETECTED | System time behind trusted time | Correct the device clock and reconnect |
| Release uncertain | Lost response or network/server refusal | Preserve identity and reconcile the seat with your software provider |
## Next steps
Review [offline mode](/docs/v1/security/offline-mode), [signed responses](/docs/v1/security/signed-responses) and [API errors](/docs/v1/api/errors).
## Related
- [Activate](/docs/v1/api/activate)
- [Validate](/docs/v1/api/validate)
- [Deactivate](/docs/v1/api/deactivate)
- [Signature test vectors](/docs/v1/security/test-vectors)
# Documentation and API changes
Canonical: https://licensing.kdpika.com/docs/v1/changelog
---
slug: "/docs/v1/changelog"
title: "Documentation and API changes"
description: "Read documentation changes separately from API release behavior and compatibility commitments."
section: "Help and notes"
order: 54
audience: "all"
level: "beginner"
last_updated: "2026-10-20"
version: "v1"
related: []
draft: false
layout: "hub"
---
## What you can do here
Read documentation changes separately from API release behavior and compatibility commitments.
- [API v1 changelog](/docs/v1/api/api-changelog) — Track source-verifiable API documentation snapshots separately from [product](/docs/v1/start/glossary#product) releases and SDK versions.
- [Versioning and deprecation](/docs/v1/api/versioning-and-deprecation) — Understand API v1 compatibility boundaries, current notification limitations and a practical upgrade checklist.
## Next steps
[Follow the Complete Integration Guide](/docs/v1/integration-guide)
# Swift REST client
Canonical: https://licensing.kdpika.com/docs/v1/sdks/swift
---
title: "Swift REST client"
description: "Build a complete Swift Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating."
slug: "/docs/v1/sdks/swift"
section: "SDKs and languages"
order: 54
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/activate", "/docs/v1/security/offline-mode"]
draft: false
---
Use the real API v1 from a trusted Swift process with explicit signature and policy checks before enabling [features](/docs/v1/start/glossary#feature). This REST sample is not an additional [official SDK](/docs/v1/start/glossary#official-sdk).
## What you will learn
- Activate, validate and deactivate one stable [Sandbox](/docs/v1/start/glossary#sandbox) installation.
- Preserve signed bytes and verify the trusted Ed25519 key before policy.
- Handle cache, [offline grace](/docs/v1/start/glossary#offline-grace), explicit rejection and safe user messages.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [signed responses](/docs/v1/security/signed-responses).
- Have the [product](/docs/v1/start/glossary#product) App ID, a Sandbox [license](/docs/v1/start/glossary#license) and its matching [customer](/docs/v1/start/glossary#customer) email when supplied.
- Obtain the actual [public-key document](/docs/v1/api/public-key) over independently trusted HTTPS and distribute/pin it through controlled application configuration, not from an untrusted envelope.
## Runtime and application type
Swift6+, macOS13+, Foundation and CryptoKit. Native macOS CLI or application service. This concrete project imports Darwin and CryptoKit; Windows/Linux compilation is not claimed. A mobile app needs its actual platform lifecycle and secure-storage adaptation. The generated Signature.swift contains the complete published verifier without its CLI tail.
## Complete project and integrity
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
[kdpika-swift-rest-example-1.0.0.zip](/docs/downloads/kdpika-swift-rest-example-1.0.0.zip) · 8789 bytes · version 1.0.0
SHA-256: ccd6c4302e9664efd1853be40a87282d0fe3baf05e8df86a562c8072d8007315
Extract the entire project, preserving its samples/sdks and samples/security layout. The archive includes all first-party client/verifier sources and pinned manifests/locks; dependency downloads are development-time restoration, never extra services needed by the PHP host. Do not flatten the tree or replace the verification helper with a generic JSON reserializer.
## Install and build
1. Verify the archive SHA-256 against the current table. Expected: exact digest match; otherwise stop.
2. Extract into a clean private development directory. Expected: complete samples tree with the language project.
3. Enter samples/sdks/swift. Expected: the main client and build/dependency files are present.
4. Install the stated runtime/native libraries, then run the full build command below. Expected: successful syntax check or native executable, not a registry SDK download.
```bash
sh build.sh
```
The source scripts compile the exact files and validate third-party download checksums where applicable. Compiler toolchains are development dependencies, not tools to install on KDPika shared hosting. macOS is the actual tested runtime; other targets require their own build/permission/TLS-root checks.
## Application configuration
| Variable | Required | Meaning |
| --- | --- | --- |
| KDP_APP_ID | Yes | [Public App ID](/docs/v1/start/glossary#public-app-id), not internal product ID or [developer](/docs/v1/start/glossary#developer) key |
| KDP_BASE_URL | No | HTTPS API base ending /api/v1; default https://licensing.kdpika.com/api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; keep it stable |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to an independently trusted public-key document |
| KDP_LICENSE_KEY | [Activation](/docs/v1/start/glossary#activation) | Sandbox key beginning TEST; never embedded in distributed source |
| KDP_CUSTOMER_EMAIL | Optional | Actual matching customer email for email-bound activation |
| KDP_ALLOW_LOOPBACK | Local testing only | 1 permits explicit localhost/127.0.0.1 HTTP, not arbitrary HTTP hosts |
Environment is fixed to SANDBOX in this example. Unsafe URL credentials/query/fragment and nonlocal HTTP are rejected. Official SDK constructors expect the site origin; these REST programs expect /api/v1. Store state and trusted keys outside public web roots. Do not ship lmp_ developer management keys.
## Complete runnable client
```swift
// Complete Sandbox session CLI. Swift6+, macOS13+, Foundation/CryptoKit.
// Build with Signature.swift derived from the published P5 verifier, no CLI tail.
// Run session status|activate|check|validate|deactivate; safe JSON decisions.
import Foundation
import CryptoKit
import Darwin
enum ClientError: Error {case setup, network, denied(String, String?)}
final class NoRedirect: NSObject, URLSessionTaskDelegate, @unchecked Sendable {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void) {completionHandler(nil)}
}
func required(_ name: String) throws -> String {
guard let value=ProcessInfo.processInfo.environment[name], !value.isEmpty, !value.contains("\r"), !value.contains("\n") else {throw ClientError.setup};return value
}
func sha(_ text: String) -> String {SHA256.hash(data:Data(text.utf8)).map{String(format:"%02x",$0)}.joined()}
func matches(_ text:String,_ pattern:String)->Bool{text.range(of:pattern,options:.regularExpression) != nil}
func object(_ raw:Data)throws->[String:Any] {
var parser=try Parser(raw);guard case .object=try parser.parse(), let value=try JSONSerialization.jsonObject(with:raw) as? [String:Any] else {throw ClientError.denied("INVALID_RESPONSE",nil)};return value
}
func instant(_ value:Any?)throws->Date {
guard let value=value as? String,matches(value,#"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$"#) else {throw ClientError.denied("INVALID_TIME",nil)}
let fractional=ISO8601DateFormatter();fractional.formatOptions=[.withInternetDateTime,.withFractionalSeconds]
guard let result=fractional.date(from:value) ?? ISO8601DateFormatter().date(from:value) else {throw ClientError.denied("INVALID_TIME",nil)};return result
}
func iso()->String{let formatter=ISO8601DateFormatter();formatter.formatOptions=[.withInternetDateTime,.withFractionalSeconds];return formatter.string(from:Date())}
func isTrue(_ value:Any?)->Bool{guard let value=value as? NSNumber else{return false};return CFGetTypeID(value)==CFBooleanGetTypeID() && value.boolValue}
import CoreFoundation
struct Stored:Codable {var token:String;var envelope:String?;var checkpoint:String}
final class Session {
let app:String,base:String,dir:URL,installation:String,key:Data
let lock:Int32
var connection:URLSession?
static func read(_ url:URL)throws->Data {
let attributes=try FileManager.default.attributesOfItem(atPath:url.path)
guard attributes[.type] as? FileAttributeType == .typeRegular, let size=attributes[.size] as? NSNumber,size.intValue<=1048576 else {throw ClientError.setup};return try Data(contentsOf:url)
}
init()throws {
app=try required("KDP_APP_ID");guard matches(app,#"^[A-Za-z0-9_-]{3,160}$"#) else {throw ClientError.setup}
let env=ProcessInfo.processInfo.environment;let raw=(env["KDP_BASE_URL"] ?? "https://licensing.kdpika.com/api/v1").replacingOccurrences(of:"/+$",with:"",options:.regularExpression)
guard let uri=URLComponents(string:raw),let host=uri.host else {throw ClientError.setup}
let local=uri.scheme=="http" && ["127.0.0.1","localhost"].contains(host) && env["KDP_ALLOW_LOOPBACK"]=="1"
guard (uri.scheme=="https" || local),uri.user==nil,uri.password==nil,uri.query==nil,uri.fragment==nil,uri.path=="/api/v1" else {throw ClientError.setup};base=raw
let root=try required("KDP_STATE_DIR"),keyfile=try required("KDP_TRUSTED_KEY_FILE");guard root.hasPrefix("/"),keyfile.hasPrefix("/") else {throw ClientError.setup}
key=try Self.read(URL(fileURLWithPath:keyfile));let publicKey=try object(key);guard publicKey["algorithm"] as? String == "Ed25519",publicKey["encoding"] as? String == "base64url" else {throw ClientError.setup}
let manager=FileManager.default;try manager.createDirectory(atPath:root,withIntermediateDirectories:true,attributes:[.posixPermissions:0o700])
guard try manager.attributesOfItem(atPath:root)[.type] as? FileAttributeType == .typeDirectory else {throw ClientError.setup}
dir=URL(fileURLWithPath:root).appendingPathComponent(String(sha(base+"|"+app+"|SANDBOX|swift-rest-v1").prefix(20)))
try manager.createDirectory(at:dir,withIntermediateDirectories:true,attributes:[.posixPermissions:0o700])
guard try manager.attributesOfItem(atPath:dir.path)[.type] as? FileAttributeType == .typeDirectory else {throw ClientError.setup};try manager.setAttributes([.posixPermissions:0o700],ofItemAtPath:dir.path)
lock=Darwin.open(dir.appendingPathComponent(".lock").path,O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW,0o600);guard lock>=0 else {throw ClientError.setup}
do {
let identity=dir.appendingPathComponent("installation.json")
if !manager.fileExists(atPath:identity.path) {let raw=try JSONSerialization.data(withJSONObject:["installation_id":"lmp-install-"+UUID().uuidString.lowercased()]);try Self.atomic(dir,"installation.json",raw)}
guard let value=try object(Self.read(identity))["installation_id"] as? String,matches(value,#"^lmp-install-[a-f0-9-]{36}$"#) else {throw ClientError.setup};installation=value
} catch {Darwin.close(lock);unlink(dir.appendingPathComponent(".lock").path);throw error}
}
func close(){connection?.invalidateAndCancel();Darwin.close(lock);unlink(dir.appendingPathComponent(".lock").path)}
static func atomic(_ directory:URL,_ name:String,_ data:Data)throws {
let temporary=directory.appendingPathComponent("write-"+UUID().uuidString);let descriptor=Darwin.open(temporary.path,O_CREAT|O_EXCL|O_WRONLY|O_NOFOLLOW,0o600)
guard descriptor>=0 else {throw ClientError.setup};let handle=FileHandle(fileDescriptor:descriptor,closeOnDealloc:true)
try handle.write(contentsOf:data);try handle.synchronize();try handle.close()
guard rename(temporary.path,directory.appendingPathComponent(name).path)==0 else {throw ClientError.setup}
}
func save(_ state:Stored)throws{try Self.atomic(dir,"session.json",JSONEncoder().encode(state))}
func state()throws->Stored? {
let url=dir.appendingPathComponent("session.json");guard FileManager.default.fileExists(atPath:url.path) else{return nil}
let raw=try Self.read(url);_=try object(raw);let state=try JSONDecoder().decode(Stored.self,from:raw)
guard matches(state.token,#"^act_[A-Za-z0-9_-]+$"#) else {throw ClientError.setup};_=try instant(state.checkpoint);return state
}
func clear()throws{let url=dir.appendingPathComponent("session.json");if FileManager.default.fileExists(atPath:url.path){try FileManager.default.removeItem(at:url)}}
func request(_ endpoint:String,_ body:[String:Any],_ token:String?=nil)async throws->(JSON,[String:Any],String?) {
guard let url=URL(string:base+endpoint) else {throw ClientError.setup};var request=URLRequest(url:url);request.httpMethod="POST";request.httpBody=try JSONSerialization.data(withJSONObject:body);request.timeoutInterval=15
request.setValue("application/json",forHTTPHeaderField:"Accept");request.setValue("application/json",forHTTPHeaderField:"Content-Type");request.setValue("KDPika-REST-Swift-example/1.0",forHTTPHeaderField:"User-Agent")
if let token=token{request.setValue("Bearer "+token,forHTTPHeaderField:"Authorization");request.setValue(installation,forHTTPHeaderField:"X-Installation-ID")}else{request.setValue(app,forHTTPHeaderField:"X-App-ID");request.setValue("SANDBOX",forHTTPHeaderField:"X-Environment")}
if connection==nil{let config=URLSessionConfiguration.ephemeral;config.httpCookieStorage=nil;config.urlCache=nil;config.timeoutIntervalForRequest=15;config.timeoutIntervalForResource=15;connection=URLSession(configuration:config,delegate:NoRedirect(),delegateQueue:nil)}
var raw=Data();var response:URLResponse
do{let (stream,reply)=try await connection!.bytes(for:request);response=reply;for try await byte in stream{raw.append(byte);if raw.count>1048576{throw ClientError.denied("INVALID_RESPONSE",nil)}}}
catch let error as ClientError {throw error} catch {throw ClientError.network}
let value:[String:Any];var tree:JSON
do{value=try object(raw);var parser=try Parser(raw);tree=try parser.parse()}catch{throw ClientError.denied("INVALID_RESPONSE",nil)}
var id=value["request_id"] as? String;if let v=id,!matches(v,#"^req_[A-Za-z0-9_-]{1,120}$"#){id=nil}
guard let http=response as? HTTPURLResponse,(200..<300).contains(http.statusCode),isTrue(value["success"]) else {let error=value["error"] as? [String:Any];let candidate=error?["code"] as? String ?? "HTTP_ERROR";throw ClientError.denied(matches(candidate,#"^[A-Z0-9_]{1,64}$"#) ? candidate:"HTTP_ERROR",id)}
return(tree,value,id)
}
func accepted(_ source:String,_ id:String?=nil)->[String:Any]{["valid":true,"source":source,"signature_valid":false,"error_code":NSNull(),"request_id":id as Any? ?? NSNull(),"pdf_export_allowed":false,"seat_count":NSNull(),"message":"Your license is ready."]}
func verify(_ envelope:String,_ source:String,_ checkpoint:String?,_ id:String?=nil)throws->[String:Any] {
guard verifyEnvelope(key,Data(envelope.utf8)) else {throw ClientError.denied("SIGNATURE_INVALID",id)}
guard let p=try object(Data(envelope.utf8))["payload"] as? [String:Any],let device=p["device"] as? [String:Any],p["product_id"] as? String==app,p["environment"] as? String=="sandbox",device["installation_hash"] as? String==sha(installation) else {throw ClientError.denied("BINDING_MISMATCH",id)}
let server=try instant(p["server_time"]),next=try instant(p["next_validation_at"]),grace=try instant(p["offline_grace_until"]),now=Date()
guard next>=server,grace>=server else {throw ClientError.denied("INVALID_TIME",id)};let latest=try checkpoint.map{max(server,try instant($0))} ?? server
guard now.addingTimeInterval(300)>=latest else {throw ClientError.denied("CLOCK_ROLLBACK_DETECTED",id)}
guard p["status"] as? String=="active" else {throw ClientError.denied("LICENSE_NOT_ACTIVE",id)}
if let start=p["starts_at"],!(start is NSNull),now < (try instant(start)){throw ClientError.denied("LICENSE_NOT_STARTED",id)}
if let expiry=p["expires_at"],!(expiry is NSNull),now >= (try instant(expiry)){throw ClientError.denied("LICENSE_EXPIRED",id)}
guard source=="online" || now<=grace else {throw ClientError.denied("OFFLINE_GRACE_EXPIRED",id)}
guard let features=p["features"] as? [String:Any] else {throw ClientError.denied("INVALID_RESPONSE",id)};var value=accepted(source,id);value["signature_valid"]=true;value["pdf_export_allowed"]=isTrue(features["pdf_export"])
if let count=features["seat_count"] as? NSNumber,CFGetTypeID(count) != CFBooleanGetTypeID(),count.doubleValue.isFinite,count.doubleValue>=0,count.doubleValue<=9007199254740991,count.doubleValue.rounded()==count.doubleValue {value["seat_count"]=count.int64Value};return value
}
func run(_ action:String)async throws->[String:Any] {
let saved=try state();if action=="status"{return ["activated":saved != nil,"installation_hash":sha(installation),"storage_security":"permission-protected-file"]}
if action=="activate"{let license=try required("KDP_LICENSE_KEY");guard license.hasPrefix("TEST") else {throw ClientError.setup};guard saved==nil else {throw ClientError.denied("ALREADY_ACTIVATED",nil)}
var body:[String:Any]=["license_key":license,"device_id":installation,"device_name":"Swift REST example","platform":"macos","app_version":"1.0.0"];if let email=ProcessInfo.processInfo.environment["KDP_CUSTOMER_EMAIL"],!email.isEmpty{body["email"]=try required("KDP_CUSTOMER_EMAIL")}
let(tree,reply,id)=try await request("/licenses/activate",body);guard let token=reply["activation_token"] as? String,matches(token,#"^act_[A-Za-z0-9_-]+$"#) else {throw ClientError.denied("INVALID_RESPONSE",id)}
do{let envelope=canonical(try tree.get("license"));let value=try verify(envelope,"online",nil,id);try save(Stored(token:token,envelope:envelope,checkpoint:iso()));return value}
catch{_=try? await request("/licenses/deactivate",[:],token);throw error}}
guard var state=saved else {throw ClientError.denied("NOT_ACTIVATED",nil)}
if action=="deactivate"{let(_,reply,id)=try await request("/licenses/deactivate",[:],state.token);guard isTrue(reply["deactivated"]) else {throw ClientError.denied("INVALID_RESPONSE",id)};try clear();var value=accepted("online",id);value["message"]="This installation has been deactivated.";return value}
if action=="check",let envelope=state.envelope{do{let value=try verify(envelope,"cache",state.checkpoint);let payload=try object(Data(envelope.utf8))["payload"] as? [String:Any];if Date() < (try instant(payload?["next_validation_at"])){state.checkpoint=iso();try save(state);return value}}catch{state.envelope=nil;try save(state)}}
do{let(tree,_,id)=try await request("/licenses/validate",[:],state.token);let envelope=canonical(try tree.get("license"));let value=try verify(envelope,"online",state.checkpoint,id);state.envelope=envelope;state.checkpoint=iso();try save(state);return value}
catch ClientError.network{guard let envelope=state.envelope else {throw ClientError.denied("OFFLINE_CACHE_MISSING",nil)};let value=try verify(envelope,"offline",state.checkpoint);state.checkpoint=iso();try save(state);return value}
catch ClientError.denied(let code,let id){if ["INVALID_TOKEN","DEVICE_MISMATCH","LICENSE_REVOKED","LICENSE_EXPIRED","LICENSE_CANCELLED"].contains(code){try clear()}else{state.envelope=nil;try save(state)};throw ClientError.denied(code,id)}
}
}
@main struct Application {
static func main()async{var code:Int32=2
do{guard CommandLine.arguments.count==2,["status","activate","check","validate","deactivate"].contains(CommandLine.arguments[1]) else {throw ClientError.setup};let session=try Session();defer{session.close()}
var value:[String:Any];do{value=try await session.run(CommandLine.arguments[1]);code=0}
catch ClientError.denied(let error,let id){value=session.accepted("local",id);value["valid"]=false;value["error_code"]=error;value["message"]="Your license could not be confirmed. Connect and try again, or contact your software provider.";code=1}
catch ClientError.network{value=session.accepted("local");value["valid"]=false;value["error_code"]="NETWORK_ERROR";value["message"]="Connect to the internet and try again.";code=1}
let raw=try JSONSerialization.data(withJSONObject:value,options:[.sortedKeys]);FileHandle.standardOutput.write(raw);FileHandle.standardOutput.write(Data([10]))
}catch{FileHandle.standardError.write(Data("Check application settings and private storage. Contact your software provider if the problem continues.\n".utf8))};exit(code)
}
}
```
The complete project also contains the exact verification helper. Its source and all positive/negative vectors are available in the [security verifier archive](/docs/downloads/security-verifiers.zip). Derived reusable files remove only the original standalone CLI entry point/imports that became unused; they do not change the signature algorithm or canonicalization.
## Run the lifecycle
Use ./session followed by exactly one action. Preserve the same App ID, state root and origin across processes.
1. Run status. Expected: activated=false for a new namespace and only an installation hash/storage category. Token presence is not a license validity decision.
2. Set a Sandbox key and matching email, then run activate. Expected: valid=true and signature_valid=true, or a safe refusal. An existing token is not silently replaced.
3. Run check. Expected: eligible authenticated cache before next_validation_at, otherwise an online validation attempt.
4. Run validate. Expected: an online attempt; only transport failure can use an eligible verified offline cache. Explicit HTTP rejection never grants offline permission.
5. Run deactivate. Expected: server-confirmed release, token/cache removal and no feature grant. valid=true here means the release operation succeeded, not permission to run a protected feature.
6. Run status again. Expected: activated=false and the same installation hash. Cumulative activation_count is not reset.
Exit0 means accepted operation/status, exit1 means refusal or connectivity denial, exit2 means unsafe configuration/state or unexpected failure. Output is safe decision metadata: never raw keys, tokens, Authorization headers or signed customer payloads.
## API contract and signature verification
Activation sends X-App-ID and X-Environment: SANDBOX plus license_key/device_id and optional matching email. Validation uses Bearer act_ and X-Installation-ID. Deactivation releases that token. See the [endpoint reference](/docs/v1/api/conventions) for full field/response/error tables.
The response parser preserves numeric lexemes, rejects duplicate/invalid JSON through the shared tested verifier, and authenticates the exact envelope. Only a verified payload enters policy. Key ID is derived from the independently trusted raw public key; wrong algorithm/key/signature is rejected. Never repair a mismatch by stripping signed fields or using JSON.stringify-style numeric normalization.
## Binding, features and fail-closed policy
The client requires its actual product App ID, environment=sandbox, SHA-256 installation binding and active status. It checks starts_at when present, expiry when nonnull, signed validation/grace dates and the last accepted checkpoint. Invalid data, status or policy is a refusal even if the payload carries a true feature.
pdf_export is allowed only for the boolean true on a fully accepted result. seat_count is exposed only as a nonnegative integer bounded by the shared safe-integer policy; otherwise it stays null. These example feature names must match your own product configuration. Constructing a result or reading token presence never authenticates a license.
## Offline grace and time checks
Before next_validation_at, check can return a verified cache. After that instant it validates online. Only a transport failure may use the existing signed state within offline_grace_until and expires_at. An explicit HTTP refusal, bad signature, malformed success or binding mismatch cannot extend access. Selected token/revocation/[device](/docs/v1/start/glossary#device)/expiry/cancellation refusals clear the token; other refusals clear cached authorization.
The last accepted checkpoint and signed server_time bound rollback with300 seconds tolerance. Grace is based on its signed deadline, not added to next_validation_at. Validating online does not promise permission if time/status/binding rules fail. No constructor flag creates an extra offline extension.
These strict sample policies are not changes to existing SDK behavior. A person controlling the host can patch the program or restore full snapshots. Use server-side authorization for high-value online actions and do not promise absolute local anti-tamper protection.
## Private storage, concurrency and recovery
The [installation ID](/docs/v1/start/glossary#installation-id) is random, persistent and independent of MAC/serial/hardware fingerprints. A namespaced private session stores the [activation token](/docs/v1/start/glossary#activation-token), lossless [signed envelope](/docs/v1/start/glossary#signed-envelope) and checkpoint. Writes are atomic and permissions are owner-only where supported; this is not automatic encryption or an OS-vault certification.
The program serializes one namespace. Java/Kotlin, Dart, Rust and the POSIX native core use an OS lock on a stable file; Go/Ruby/Swift use exclusive lock-file ownership and require manual reconciliation after a crash. Never delete an active lock or regenerate identity to avoid a refusal. A new identity can consume another cumulative activation. Keep separate namespaces for unrelated buyers/tenants.
Review [secure token storage](/docs/v1/security/secure-token-storage) for native vault adapters. POSIX modes do not replace Windows ACL review. Do not put state under public_html, bundle a populated cache, or expose raw SDK/client methods to a renderer/browser.
## Errors and uncertain mutations
The client accepts only bounded safe error/request identifiers and shows plain user messages. Unknown errors fail closed. No automatic mutation retry is supplied. A lost activation response may already have consumed a server activation; reconcile before retrying. If a returned new token fails signature/policy acceptance, the client attempts best-effort release, which can itself remain uncertain.
Read [the complete server error table](/docs/v1/api/errors). Local example identifiers such as BINDING_MISMATCH, ALREADY_ACTIVATED, SIGNATURE_INVALID and INVALID_TIME are client policy results, not invented server emissions. Never show raw stack traces, private paths or customer payloads to end users.
## Packaging and framework integration
Native macOS CLI or application service. This concrete project imports Darwin and CryptoKit; Windows/Linux compilation is not claimed. A mobile app needs its actual platform lifecycle and secure-storage adaptation. The generated Signature.swift contains the complete published verifier without its CLI tail.
Keep stable state outside executable extraction/install directories. Include the exact verification modules and required native/runtime libraries; never embed a developer key or populated token/cache. A web route still needs application authentication, tenant authorization and CSRF protection. A desktop renderer gets narrow safe decisions only, not arbitrary file paths or token access.
Re-test real activation, cache reload, offline loss, explicit refusal and release after bundling or updating dependencies. macOS native source execution is not proof that every operating-system package, signing/notarization setup or framework UI has passed.
## Recorded verification and limits
The exact native runtime executes the real isolated Sandbox lifecycle and21 negative/positive checks: wrong email, stable cross-process identity, cache without HTTP, online validation, injected network loss, modified offline cache, rollback, explicit HTTP503, modified signed response, no-follow307, recovery, release and unsafe setup guards. Refusal/outage/redirect injections are an explicit loopback test forwarder, not a claimed production outage.
Build evidence records actual toolchain/dependency hashes. All ten fresh project archives build and the complete native clients execute real Sandbox lifecycles. All23 published signature vectors pass in every REST language. Signature authenticity is separate from current license authorization. Native Windows/Linux/mobile GUI execution is not inferred from this macOS run.
## Expected result
Your application authenticates signed state, applies binding/time/status policy and enables only verified features. A refusal or successful seat release cannot accidentally unlock a feature.
## Common mistakes
- Using the internal product ID instead of the Public App ID.
- Losing numeric tokens before verifying the signature.
- Treating a true feature or HTTP success as a validity proof.
- Sharing one installation cache between unrelated tenants or deleting identity on failure.
## Security notes
Keep HTTPS verification, independent key trust and fail-closed policy. This example does not change the platform contract, process payments or grant universal anti-tamper protection. Preserve real API/SDK boundaries and keep credentials off browser/renderer surfaces.
## Troubleshooting
| Symptom | Cause | Action |
| --- | --- | --- |
| Exit2 at startup | Unsafe URL, missing private path, bad identity or busy lock | Correct trusted configuration; do not expose raw diagnostics to users |
| INVALID_LICENSE | Key/email/App ID/environment mismatch | Use the actual matching Sandbox values |
| SIGNATURE_INVALID | Wrong key, modified state or changed canonical bytes | Deny access and run the published vectors |
| OFFLINE_CACHE_MISSING | No accepted signed state remains | Reconnect and validate; never manufacture a cache |
| CLOCK_ROLLBACK_DETECTED | System time behind trusted time | Correct the device clock and reconnect |
| Release uncertain | Lost response or network/server refusal | Preserve identity and reconcile the seat with your software provider |
## Next steps
Review [offline mode](/docs/v1/security/offline-mode), [signed responses](/docs/v1/security/signed-responses) and [API errors](/docs/v1/api/errors).
## Related
- [Activate](/docs/v1/api/activate)
- [Validate](/docs/v1/api/validate)
- [Deactivate](/docs/v1/api/deactivate)
- [Signature test vectors](/docs/v1/security/test-vectors)
# Dart REST client
Canonical: https://licensing.kdpika.com/docs/v1/sdks/dart
---
title: "Dart REST client"
description: "Build a complete Dart Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating."
slug: "/docs/v1/sdks/dart"
section: "SDKs and languages"
order: 55
audience: "all"
level: "intermediate"
last_updated: "2026-10-17"
version: "v1"
related: ["/docs/v1/api/activate", "/docs/v1/security/offline-mode"]
draft: false
---
Use the real API v1 from a trusted Dart process with explicit signature and policy checks before enabling [features](/docs/v1/start/glossary#feature). This REST sample is not an additional [official SDK](/docs/v1/start/glossary#official-sdk).
## What you will learn
- Activate, validate and deactivate one stable [Sandbox](/docs/v1/start/glossary#sandbox) installation.
- Preserve signed bytes and verify the trusted Ed25519 key before policy.
- Handle cache, [offline grace](/docs/v1/start/glossary#offline-grace), explicit rejection and safe user messages.
## Prerequisites
- Complete [the Sandbox quickstart](/docs/v1/start/quickstart).
- Read [authentication](/docs/v1/start/authentication) and [signed responses](/docs/v1/security/signed-responses).
- Have the [product](/docs/v1/start/glossary#product) App ID, a Sandbox [license](/docs/v1/start/glossary#license) and its matching [customer](/docs/v1/start/glossary#customer) email when supplied.
- Obtain the actual [public-key document](/docs/v1/api/public-key) over independently trusted HTTPS and distribute/pin it through controlled application configuration, not from an untrusted envelope.
## Runtime and application type
Dart3.7+, cryptography2.9.0 with pubspec.lock. Dart VM CLI or trusted backend; not browser Dart. Native Dart file locks serialize a namespace. On POSIX this sample calls the fixed /bin/chmod tool to set permissions; Windows needs application ACL review. Flutter mobile/desktop UI integration and platform vaults are separate app responsibilities.
## Complete project and integrity
The complete project archive contains every client/verifier source and the exact build/dependency metadata needed by this language. Extract it with its original directory layout. Dependencies/locks are included where this runtime uses them; no hidden helper or test-only clock is required.
[kdpika-dart-rest-example-1.0.0.zip](/docs/downloads/kdpika-dart-rest-example-1.0.0.zip) · 8942 bytes · version 1.0.0
SHA-256: 729a44db23dcf31fab34ab9cd2cc17b89f74d6f83d477c7ca9e70d7eabaa2742
Extract the entire project, preserving its samples/sdks and samples/security layout. The archive includes all first-party client/verifier sources and pinned manifests/locks; dependency downloads are development-time restoration, never extra services needed by the PHP host. Do not flatten the tree or replace the verification helper with a generic JSON reserializer.
## Install and build
1. Verify the archive SHA-256 against the current table. Expected: exact digest match; otherwise stop.
2. Extract into a clean private development directory. Expected: complete samples tree with the language project.
3. Enter samples/sdks/dart. Expected: the main client and build/dependency files are present.
4. Install the stated runtime/native libraries, then run the full build command below. Expected: successful syntax check or native executable, not a registry SDK download.
```bash
dart pub get --enforce-lockfile
dart analyze session.dart
dart compile exe session.dart -o session
```
The source scripts compile the exact files and validate third-party download checksums where applicable. Compiler toolchains are development dependencies, not tools to install on KDPika shared hosting. macOS is the actual tested runtime; other targets require their own build/permission/TLS-root checks.
## Application configuration
| Variable | Required | Meaning |
| --- | --- | --- |
| KDP_APP_ID | Yes | [Public App ID](/docs/v1/start/glossary#public-app-id), not internal product ID or [developer](/docs/v1/start/glossary#developer) key |
| KDP_BASE_URL | No | HTTPS API base ending /api/v1; default https://licensing.kdpika.com/api/v1 |
| KDP_STATE_DIR | Yes | Absolute private application state root; keep it stable |
| KDP_TRUSTED_KEY_FILE | Yes | Absolute path to an independently trusted public-key document |
| KDP_LICENSE_KEY | [Activation](/docs/v1/start/glossary#activation) | Sandbox key beginning TEST; never embedded in distributed source |
| KDP_CUSTOMER_EMAIL | Optional | Actual matching customer email for email-bound activation |
| KDP_ALLOW_LOOPBACK | Local testing only | 1 permits explicit localhost/127.0.0.1 HTTP, not arbitrary HTTP hosts |
Environment is fixed to SANDBOX in this example. Unsafe URL credentials/query/fragment and nonlocal HTTP are rejected. Official SDK constructors expect the site origin; these REST programs expect /api/v1. Store state and trusted keys outside public web roots. Do not ship lmp_ developer management keys.
## Complete runnable client
```dart
// Complete Sandbox REST CLI. Dart3.7+, cryptography2.9.0, dart:io.
// Run dart run session.dart status|activate|check|validate|deactivate.
// Uses exact published P5 verifier in verifier.dart; output safe decisions only.
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:cryptography/cryptography.dart';
import 'verifier.dart' as signature;
class Refusal implements Exception {final String code;final String? requestId;Refusal(this.code,[this.requestId]);}
class Unreachable implements Exception {}
String required(String name){final value=Platform.environment[name]??'';if(value.isEmpty||value.contains('\r')||value.contains('\n'))throw StateError('configuration');return value;}
Map object(List raw){final value=signature.Parser(raw).parse();if(value is! Map)throw Refusal('INVALID_RESPONSE');return value;}
Future hash(String text)async{final value=await Sha256().hash(utf8.encode(text));return value.bytes.map((b)=>b.toRadixString(16).padLeft(2,'0')).join();}
String randomHex(int length){final random=Random.secure();return List.generate(length,(_)=>random.nextInt(256).toRadixString(16).padLeft(2,'0')).join();}
DateTime instant(dynamic value){if(value is! String||!RegExp(r'^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$').hasMatch(value))throw Refusal('INVALID_TIME');try{return DateTime.parse(value).toUtc();}catch(_){throw Refusal('INVALID_TIME');}}
Future> readPrivate(String path)async{final type=await FileSystemEntity.type(path,followLinks:false);if(type!=FileSystemEntityType.file||await File(path).length()>1048576)throw StateError('unsafe file');return File(path).readAsBytes();}
Future chmod(String path,String mode)async{if(!Platform.isWindows){final result=await Process.run('/bin/chmod',[mode,path]);if(result.exitCode!=0)throw StateError('permissions');}}
class Session {
late String app,base,directory,installation;late List trusted;
RandomAccessFile? lock;String? lockPath;
final HttpClient http=HttpClient()..connectionTimeout=const Duration(seconds:5);
static Future open()async{
final client=Session();try{await client.initialize();return client;}catch(_){await client.close();rethrow;}
}
Future initialize()async{
app=required('KDP_APP_ID');if(!RegExp(r'^[A-Za-z0-9_-]{3,160}$').hasMatch(app))throw StateError('app ID');
base=(Platform.environment['KDP_BASE_URL']??'https://licensing.kdpika.com/api/v1').replaceFirst(RegExp(r'/+$'),'');final uri=Uri.parse(base);
final local=uri.scheme=='http'&&['localhost','127.0.0.1'].contains(uri.host)&&Platform.environment['KDP_ALLOW_LOOPBACK']=='1';
if(uri.host.isEmpty||(uri.scheme!='https'&&!local)||uri.userInfo.isNotEmpty||uri.hasQuery||uri.hasFragment||uri.path!='/api/v1')throw StateError('URL');
final root=required('KDP_STATE_DIR'),key=required('KDP_TRUSTED_KEY_FILE');if(!Directory(root).isAbsolute||!File(key).isAbsolute)throw StateError('absolute paths');
trusted=await readPrivate(key);final pub=object(trusted);if(pub['algorithm']!='Ed25519'||pub['encoding']!='base64url')throw StateError('trusted key');
await Directory(root).create(recursive:true);if(await FileSystemEntity.type(root,followLinks:false)!=FileSystemEntityType.directory)throw StateError('root symlink');
directory='$root/${(await hash('$base|$app|SANDBOX|dart-rest-v1')).substring(0,20)}';await Directory(directory).create(recursive:true);
if(await FileSystemEntity.type(directory,followLinks:false)!=FileSystemEntityType.directory)throw StateError('namespace symlink');await chmod(directory,'700');
// OS lock stays on one stable lock file; do not unlink it while another
// process may wait on its inode. An active owner fails immediately.
lockPath='$directory/.lock';if(await FileSystemEntity.type(lockPath!,followLinks:false)==FileSystemEntityType.link)throw StateError('lock symlink');
lock=await File(lockPath!).open(mode:FileMode.append);await chmod(lockPath!,'600');await lock!.lock(FileLock.exclusive);
final identity='$directory/installation.json';
if(await FileSystemEntity.type(identity,followLinks:false)==FileSystemEntityType.notFound){final hex=randomHex(16);await write('installation.json',{'installation_id':'lmp-install-${hex.substring(0,8)}-${hex.substring(8,12)}-${hex.substring(12,16)}-${hex.substring(16,20)}-${hex.substring(20)}'});}
final value=object(await readPrivate(identity));installation=value['installation_id'] as String;
if(!RegExp(r'^lmp-install-[a-f0-9-]{36}$').hasMatch(installation))throw StateError('invalid identity; do not reset automatically');
}
Future close()async{http.close(force:true);if(lock!=null){try{await lock!.unlock();}catch(_){}await lock!.close();lock=null;}}
Future write(String name,dynamic value)async{final temporary=File('$directory/write-${randomHex(12)}');await temporary.create(exclusive:true);await chmod(temporary.path,'600');await temporary.writeAsString(jsonEncode(value),flush:true);await temporary.rename('$directory/$name');}
Future