# 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.](/docs/diagrams/docs-pipeline.svg) 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.](/docs/diagrams/p4-architecture.svg) 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.0 enableenable false ``` ```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.](/docs/diagrams/p4-concepts.svg) 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.](/docs/diagrams/p4-lifecycle.svg) 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.](/docs/diagrams/p4-flow-first.svg) 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.](/docs/diagrams/p4-flow-normal.svg) 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.](/docs/diagrams/p4-flow-offline.svg) 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.](/docs/diagrams/p4-flow-revoked.svg) 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.](/docs/diagrams/p4-flow-expired.svg) 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.](/docs/diagrams/p4-flow-limit.svg) 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.](/docs/diagrams/p5-offline.svg) 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 Exe net7.0 enable enable ``` ```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 Exe net7.0 enable enable ``` ```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.0 enableenable false ``` ```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 Exe net7.0 enable enable ``` 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` 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 Exe net7.0 enable enable ``` 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 Exe net7.0 enable enable ``` 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` ## 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 Exe net7.0 enable enable ``` ## 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.0 enableenable ``` ::: ::: 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.0 enableenable false ``` 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 Exe net7.0 enable enable false ``` ## 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?> state()async{final file='$directory/session.json';if(await FileSystemEntity.type(file,followLinks:false)==FileSystemEntityType.notFound)return null; final raw=await readPrivate(file);object(raw);final value=jsonDecode(utf8.decode(raw)) as Map; if(value['token'] is! String||!RegExp(r'^act_[A-Za-z0-9_-]+$').hasMatch(value['token'])||(value['envelope']!=null&&value['envelope'] is! String))throw StateError('stored session');instant(value['checkpoint']);return value; } Future clear()async{final file=File('$directory/session.json');if(await file.exists())await file.delete();} Future<(Map,String?)> request(String endpoint,Map body,[String? token])async{ late HttpClientResponse response;final raw=[]; try{ final request=await http.postUrl(Uri.parse(base+endpoint)).timeout(const Duration(seconds:15));request.followRedirects=false; request.headers.set('Accept','application/json');request.headers.set('Content-Type','application/json');request.headers.set('User-Agent','KDPika-REST-Dart-example/1.0'); if(token==null){request.headers.set('X-App-ID',app);request.headers.set('X-Environment','SANDBOX');}else{request.headers.set('Authorization','Bearer $token');request.headers.set('X-Installation-ID',installation);} final encoded=utf8.encode(jsonEncode(body));request.contentLength=encoded.length;request.add(encoded);response=await request.close().timeout(const Duration(seconds:15)); await for(final bytes in response.timeout(const Duration(seconds:15))){raw.addAll(bytes);if(raw.length>1048576)throw Refusal('INVALID_RESPONSE');} }on Refusal{rethrow;}catch(_){throw Unreachable();} late Map value;try{value=object(raw);}catch(_){throw Refusal('INVALID_RESPONSE');} var id=value['request_id'] as String?;if(id!=null&&!RegExp(r'^req_[A-Za-z0-9_-]{1,120}$').hasMatch(id))id=null; if(response.statusCode<200||response.statusCode>=300||value['success']!=true){final error=value['error'];final candidate=error is Map?error['code']:null;throw Refusal(candidate is String&&RegExp(r'^[A-Z0-9_]{1,64}$').hasMatch(candidate)?candidate:'HTTP_ERROR',id);} return(value,id); } Map accepted(String source,[String? id])=>{'valid':true,'source':source,'signature_valid':false,'error_code':null,'request_id':id,'pdf_export_allowed':false,'seat_count':null,'message':'Your license is ready.'}; Future> verify(String envelope,String source,String? checkpoint,[String? id])async{ if(!await signature.verifyEnvelope(trusted,utf8.encode(envelope)))throw Refusal('SIGNATURE_INVALID',id); final payload=(jsonDecode(envelope) as Map)['payload'] as Map; if(payload['product_id']!=app||payload['environment']!='sandbox'||payload['device'] is! Map||payload['device']['installation_hash']!=await hash(installation))throw Refusal('BINDING_MISMATCH',id); final server=instant(payload['server_time']),next=instant(payload['next_validation_at']),grace=instant(payload['offline_grace_until']); if(next.isBefore(server)||grace.isBefore(server))throw Refusal('INVALID_TIME',id);var latest=server;if(checkpoint!=null&&instant(checkpoint).isAfter(latest))latest=instant(checkpoint);final now=DateTime.now().toUtc(); if(now.add(const Duration(seconds:300)).isBefore(latest))throw Refusal('CLOCK_ROLLBACK_DETECTED',id); if(payload['status']!='active')throw Refusal('LICENSE_NOT_ACTIVE',id); if(payload['starts_at']!=null&&now.isBefore(instant(payload['starts_at'])))throw Refusal('LICENSE_NOT_STARTED',id); if(payload['expires_at']!=null&&!now.isBefore(instant(payload['expires_at'])))throw Refusal('LICENSE_EXPIRED',id); if(source!='online'&&now.isAfter(grace))throw Refusal('OFFLINE_GRACE_EXPIRED',id); final features=payload['features'];if(features is! Map)throw Refusal('INVALID_RESPONSE',id);final value=accepted(source,id);value['signature_valid']=true;value['pdf_export_allowed']=features['pdf_export']==true; final count=features['seat_count'];if(count is int&&count>=0&&count<=9007199254740991)value['seat_count']=count;return value; } Future> run(String action)async{ final saved=await state();if(action=='status')return {'activated':saved!=null,'installation_hash':await hash(installation),'storage_security':'permission-protected-file'}; if(action=='activate'){final license=required('KDP_LICENSE_KEY');if(!license.startsWith('TEST'))throw StateError('Sandbox key');if(saved!=null)throw Refusal('ALREADY_ACTIVATED'); final body={'license_key':license,'device_id':installation,'device_name':'Dart REST example','platform':Platform.operatingSystem,'app_version':'1.0.0'};if((Platform.environment['KDP_CUSTOMER_EMAIL']??'').isNotEmpty)body['email']=required('KDP_CUSTOMER_EMAIL'); final(reply,id)=await request('/licenses/activate',body);final token=reply['activation_token'];if(token is! String||!RegExp(r'^act_[A-Za-z0-9_-]+$').hasMatch(token))throw Refusal('INVALID_RESPONSE',id); try{final envelope=signature.canonical(reply['license']);final value=await verify(envelope,'online',null,id);await write('session.json',{'token':token,'envelope':envelope,'checkpoint':DateTime.now().toUtc().toIso8601String()});return value;} catch(_){try{await request('/licenses/deactivate',{},token);}catch(_){}rethrow;}} if(saved==null)throw Refusal('NOT_ACTIVATED');final token=saved['token'] as String; if(action=='deactivate'){final(reply,id)=await request('/licenses/deactivate',{},token);if(reply['deactivated']!=true)throw Refusal('INVALID_RESPONSE',id);await clear();final value=accepted('online',id);value['message']='This installation has been deactivated.';return value;} if(action=='check'&&saved['envelope']!=null){try{final value=await verify(saved['envelope'],'cache',saved['checkpoint']);final payload=jsonDecode(saved['envelope'])['payload'];if(DateTime.now().toUtc().isBefore(instant(payload['next_validation_at']))){saved['checkpoint']=DateTime.now().toUtc().toIso8601String();await write('session.json',saved);return value;}} on Refusal{saved['envelope']=null;await write('session.json',saved);}} late Map value; try{final(reply,id)=await request('/licenses/validate',{},token);final envelope=signature.canonical(reply['license']);value=await verify(envelope,'online',saved['checkpoint'],id);saved['envelope']=envelope;} on Unreachable{if(saved['envelope']==null)throw Refusal('OFFLINE_CACHE_MISSING');value=await verify(saved['envelope'],'offline',saved['checkpoint']);} on Refusal catch(error){if(['INVALID_TOKEN','DEVICE_MISMATCH','LICENSE_REVOKED','LICENSE_EXPIRED','LICENSE_CANCELLED'].contains(error.code)){await clear();}else{saved['envelope']=null;await write('session.json',saved);}rethrow;} saved['checkpoint']=DateTime.now().toUtc().toIso8601String();await write('session.json',saved);return value; } } Future main(List args)async{Session? client;var code=2; try{if(args.length!=1||!['status','activate','check','validate','deactivate'].contains(args[0]))throw StateError('action');client=await Session.open(); try{print(jsonEncode(await client.run(args[0])));code=0;}on Refusal catch(error){final value=client.accepted('local',error.requestId);value['valid']=false;value['error_code']=error.code;value['message']='Your license could not be confirmed. Connect and try again, or contact your software provider.';print(jsonEncode(value));code=1;} on Unreachable{final value=client.accepted('local');value['valid']=false;value['error_code']='NETWORK_ERROR';value['message']='Connect to the internet and try again.';print(jsonEncode(value));code=1;} }catch(_){stderr.writeln('Check application settings and private storage. Contact your software provider if the problem continues.');}finally{await client?.close();}exitCode=code; } ``` ```yaml name: kdpika_dart_rest_example version: 1.0.0 publish_to: none environment: sdk: '>=3.7.0 <4.0.0' dependencies: cryptography: 2.9.0 ``` 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 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. 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) # Go REST client Canonical: https://licensing.kdpika.com/docs/v1/sdks/go --- title: "Go REST client" description: "Build a complete Go Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating." slug: "/docs/v1/sdks/go" section: "SDKs and languages" order: 56 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 Go 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 Go1.22+, standard library only. Native CLI or backend, without third-party Go runtime modules. Build for each target and validate its TLS roots/ACLs. Its reusable signature.go is derived from the complete P5 verifier with only the original CLI main and unused os import removed. An exclusive-create lock refuses concurrent writers; reconcile a stale lock after confirming its process ended. ## 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-go-rest-example-1.0.0.zip](/docs/downloads/kdpika-go-rest-example-1.0.0.zip) · 8141 bytes · version 1.0.0 SHA-256: 2d034b773ed49c069553ab3f4f97d63ea528f11c784b01ba14ac3ff87bdd25e8 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/go. 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 ```go // Complete Sandbox REST CLI; Go1.22+, standard library only. // Build with adjacent signature.go derived byte-for-byte from the P5 verifier // without its CLI main. Run session status|activate|check|validate|deactivate. package main import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "os" "path/filepath" "regexp" "strings" "time" ) type refusal struct{code string;requestID string} func (r refusal) Error()string{return r.code} type unreachable struct{} func (unreachable) Error()string{return "network unavailable"} type stored struct{Token string `json:"token"`;Envelope *string `json:"envelope"`;Checkpoint string `json:"checkpoint"`} type session struct{app,base,dir,installation string;key []byte;lock *os.File;http *http.Client} func hash(v string)string{sum:=sha256.Sum256([]byte(v));return hex.EncodeToString(sum[:])} func required(name string)string{v:=os.Getenv(name);if v==""||strings.ContainsAny(v,"\r\n"){panic("configuration")};return v} func readPrivate(path string)([]byte,error){stat,err:=os.Lstat(path);if err!=nil{return nil,err};if !stat.Mode().IsRegular()||stat.Size()>1048576{return nil,errors.New("unsafe file")};return os.ReadFile(path)} func jsonObject(raw []byte)(map[string]any,error){v,e:=parse(raw);if e!=nil{return nil,e};m,ok:=v.(map[string]any);if !ok{return nil,errors.New("object required")};return m,nil} func text(m map[string]any,k string)string{v,ok:=m[k].(string);if !ok{panic("missing string")};return v} func parseTime(v string)(time.Time,error){t,e:=time.Parse(time.RFC3339Nano,v);if e!=nil{return t,refusal{"INVALID_TIME",""}};return t,nil} func (s *session) write(name string,v any)error{ raw,e:=json.Marshal(v);if e!=nil{return e};file,e:=os.CreateTemp(s.dir,"write-");if e!=nil{return e};temporary:=file.Name() if e=file.Chmod(0600);e==nil{_,e=file.Write(raw)};if e==nil{e=file.Sync()};closeError:=file.Close();if e==nil{e=closeError};if e!=nil{return e};return os.Rename(temporary,filepath.Join(s.dir,name)) } func openSession()(*session,error){ s:=&session{app:required("KDP_APP_ID")};if !regexp.MustCompile(`^[A-Za-z0-9_-]{3,160}$`).MatchString(s.app){return nil,errors.New("app ID")} base:=os.Getenv("KDP_BASE_URL");if base==""{base="https://licensing.kdpika.com/api/v1"};u,e:=url.Parse(strings.TrimRight(base,"/"));if e!=nil{return nil,e} 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"{return nil,errors.New("URL")};s.base=u.String() root,keyfile:=required("KDP_STATE_DIR"),required("KDP_TRUSTED_KEY_FILE");if !filepath.IsAbs(root)||!filepath.IsAbs(keyfile){return nil,errors.New("absolute paths")} s.key,e=readPrivate(keyfile);if e!=nil{return nil,e};key,e:=jsonObject(s.key);if e!=nil||key["algorithm"]!="Ed25519"||key["encoding"]!="base64url"{return nil,errors.New("key")} if e=os.MkdirAll(root,0700);e!=nil{return nil,e};info,e:=os.Lstat(root);if e!=nil||!info.IsDir()||info.Mode()&os.ModeSymlink!=0{return nil,errors.New("root")} s.dir=filepath.Join(root,hash(s.base+"|"+s.app+"|SANDBOX|go-rest-v1")[:20]);if e=os.MkdirAll(s.dir,0700);e!=nil{return nil,e} info,e=os.Lstat(s.dir);if e!=nil||!info.IsDir()||info.Mode()&os.ModeSymlink!=0{return nil,errors.New("namespace")};if e=os.Chmod(s.dir,0700);e!=nil{return nil,e} s.lock,e=os.OpenFile(filepath.Join(s.dir,".lock"),os.O_CREATE|os.O_EXCL|os.O_WRONLY,0600);if e!=nil{return nil,e} success:=false;defer func(){if !success{s.close()}}() identity:=filepath.Join(s.dir,"installation.json");raw,e:=readPrivate(identity) if os.IsNotExist(e){random:=make([]byte,16);if _,e=rand.Read(random);e!=nil{return nil,e};random[6]=(random[6]&15)|64;random[8]=(random[8]&63)|128 id:=fmt.Sprintf("lmp-install-%x-%x-%x-%x-%x",random[:4],random[4:6],random[6:8],random[8:10],random[10:]);if e=s.write("installation.json",map[string]string{"installation_id":id});e!=nil{return nil,e};raw,e=readPrivate(identity)} if e!=nil{return nil,e};value,e:=jsonObject(raw);if e!=nil{return nil,e};s.installation=text(value,"installation_id") if !regexp.MustCompile(`^lmp-install-[a-f0-9-]{36}$`).MatchString(s.installation){return nil,errors.New("invalid identity; do not reset automatically")} s.http=&http.Client{Timeout:15*time.Second,CheckRedirect:func(_ *http.Request,_ []*http.Request)error{return http.ErrUseLastResponse}};success=true;return s,nil } func(s *session)close(){if s.lock!=nil{s.lock.Close();os.Remove(filepath.Join(s.dir,".lock"));s.lock=nil}} func(s *session)state()(*stored,error){raw,e:=readPrivate(filepath.Join(s.dir,"session.json"));if os.IsNotExist(e){return nil,nil};if e!=nil{return nil,e};if _,e=jsonObject(raw);e!=nil{return nil,e};var value stored if e=json.Unmarshal(raw,&value);e!=nil{return nil,e};if !regexp.MustCompile(`^act_[A-Za-z0-9_-]+$`).MatchString(value.Token){return nil,errors.New("stored token")};if _,e=parseTime(value.Checkpoint);e!=nil{return nil,e};return &value,nil} func(s *session)clear()error{e:=os.Remove(filepath.Join(s.dir,"session.json"));if os.IsNotExist(e){return nil};return e} func(s *session)request(endpoint string,body map[string]any,token string)(map[string]any,string,error){ raw,e:=json.Marshal(body);if e!=nil{return nil,"",e};req,e:=http.NewRequest("POST",s.base+endpoint,strings.NewReader(string(raw)));if e!=nil{return nil,"",e} req.Header.Set("Content-Type","application/json");req.Header.Set("Accept","application/json");req.Header.Set("User-Agent","KDPika-REST-Go-example/1.0") if token==""{req.Header.Set("X-App-ID",s.app);req.Header.Set("X-Environment","SANDBOX")}else{req.Header.Set("Authorization","Bearer "+token);req.Header.Set("X-Installation-ID",s.installation)} response,e:=s.http.Do(req);if e!=nil{return nil,"",unreachable{}};defer response.Body.Close();raw,e=io.ReadAll(io.LimitReader(response.Body,1048577));if e!=nil{return nil,"",unreachable{}};if len(raw)>1048576{return nil,"",refusal{"INVALID_RESPONSE",""}} value,e:=jsonObject(raw);if e!=nil{return nil,"",refusal{"INVALID_RESPONSE",""}};id,_:=value["request_id"].(string);if !regexp.MustCompile(`^req_[A-Za-z0-9_-]{1,120}$`).MatchString(id){id=""} if response.StatusCode<200||response.StatusCode>=300||value["success"]!=true{code:="HTTP_ERROR";if errorObject,ok:=value["error"].(map[string]any);ok{if c,ok:=errorObject["code"].(string);ok&®exp.MustCompile(`^[A-Z0-9_]{1,64}$`).MatchString(c){code=c}};return nil,id,refusal{code,id}} return value,id,nil } func success(source,id string)map[string]any{var request any;if id!=""{request=id};return map[string]any{"valid":true,"source":source,"signature_valid":false,"error_code":nil,"request_id":request,"pdf_export_allowed":false,"seat_count":nil,"message":"Your license is ready."}} func(s *session)verify(envelope,source,checkpoint,id string)(map[string]any,error){ if !VerifyEnvelope(s.key,[]byte(envelope)){return nil,refusal{"SIGNATURE_INVALID",id}};env,e:=jsonObject([]byte(envelope));if e!=nil{return nil,e};p,ok:=env["payload"].(map[string]any);if !ok{return nil,refusal{"INVALID_RESPONSE",id}};device,ok:=p["device"].(map[string]any) if !ok||p["product_id"]!=s.app||p["environment"]!="sandbox"||device["installation_hash"]!=hash(s.installation){return nil,refusal{"BINDING_MISMATCH",id}} server,e:=parseTime(text(p,"server_time"));if e!=nil{return nil,e};next,e:=parseTime(text(p,"next_validation_at"));if e!=nil{return nil,e};grace,e:=parseTime(text(p,"offline_grace_until"));if e!=nil{return nil,e};if next.Before(server)||grace.Before(server){return nil,refusal{"INVALID_TIME",id}} latest:=server;if checkpoint!=""{previous,e:=parseTime(checkpoint);if e!=nil{return nil,e};if previous.After(latest){latest=previous}};now:=time.Now() if now.Add(300*time.Second).Before(latest){return nil,refusal{"CLOCK_ROLLBACK_DETECTED",id}};if p["status"]!="active"{return nil,refusal{"LICENSE_NOT_ACTIVE",id}} if p["starts_at"]!=nil{start,e:=parseTime(text(p,"starts_at"));if e!=nil{return nil,e};if now.Before(start){return nil,refusal{"LICENSE_NOT_STARTED",id}}} if p["expires_at"]!=nil{expiry,e:=parseTime(text(p,"expires_at"));if e!=nil{return nil,e};if !now.Before(expiry){return nil,refusal{"LICENSE_EXPIRED",id}}} if source!="online"&&now.After(grace){return nil,refusal{"OFFLINE_GRACE_EXPIRED",id}};features,ok:=p["features"].(map[string]any);if !ok{return nil,refusal{"INVALID_RESPONSE",id}} result:=success(source,id);result["signature_valid"]=true;result["pdf_export_allowed"]=features["pdf_export"]==true if count,ok:=features["seat_count"].(json.Number);ok{if integer,e:=count.Int64();e==nil&&integer>=0&&integer<=9007199254740991{result["seat_count"]=integer}};return result,nil } func(s *session)run(action string)(map[string]any,error){ state,e:=s.state();if e!=nil{return nil,e};if action=="status"{return map[string]any{"activated":state!=nil,"installation_hash":hash(s.installation),"storage_security":"permission-protected-file"},nil} if action=="activate"{license:=required("KDP_LICENSE_KEY");if !strings.HasPrefix(license,"TEST"){return nil,errors.New("Sandbox key required")};if state!=nil{return nil,refusal{"ALREADY_ACTIVATED",""}} body:=map[string]any{"license_key":license,"device_id":s.installation,"device_name":"Go REST example","platform":"go","app_version":"1.0.0"};if os.Getenv("KDP_CUSTOMER_EMAIL")!=""{body["email"]=required("KDP_CUSTOMER_EMAIL")} response,id,e:=s.request("/licenses/activate",body,"");if e!=nil{return nil,e};token,ok:=response["activation_token"].(string);if !ok||!regexp.MustCompile(`^act_[A-Za-z0-9_-]+$`).MatchString(token){return nil,refusal{"INVALID_RESPONSE",id}} envelope:=canonical(response["license"]);result,e:=s.verify(envelope,"online","",id);if e!=nil{s.request("/licenses/deactivate",map[string]any{},token);return nil,e} return result,s.write("session.json",stored{token,&envelope,time.Now().UTC().Format(time.RFC3339Nano)})} if state==nil{return nil,refusal{"NOT_ACTIVATED",""}};if action=="deactivate"{response,id,e:=s.request("/licenses/deactivate",map[string]any{},state.Token);if e!=nil{return nil,e};if response["deactivated"]!=true{return nil,refusal{"INVALID_RESPONSE",id}};if e=s.clear();e!=nil{return nil,e};result:=success("online",id);result["message"]="This installation has been deactivated.";return result,nil} if action=="check"&&state.Envelope!=nil{result,e:=s.verify(*state.Envelope,"cache",state.Checkpoint,"");if e==nil{env,_:=jsonObject([]byte(*state.Envelope));p:=env["payload"].(map[string]any);next,_:=parseTime(text(p,"next_validation_at"));if time.Now().Before(next){state.Checkpoint=time.Now().UTC().Format(time.RFC3339Nano);return result,s.write("session.json",state)}}else{state.Envelope=nil;if e=s.write("session.json",state);e!=nil{return nil,e}}} response,id,e:=s.request("/licenses/validate",map[string]any{},state.Token) if e!=nil{var offline unreachable;if errors.As(e,&offline){if state.Envelope==nil{return nil,refusal{"OFFLINE_CACHE_MISSING",""}};result,e:=s.verify(*state.Envelope,"offline",state.Checkpoint,"");if e!=nil{return nil,e};state.Checkpoint=time.Now().UTC().Format(time.RFC3339Nano);return result,s.write("session.json",state)} var denied refusal;if errors.As(e,&denied){state.Envelope=nil;if denied.code=="INVALID_TOKEN"||denied.code=="DEVICE_MISMATCH"||denied.code=="LICENSE_REVOKED"||denied.code=="LICENSE_EXPIRED"||denied.code=="LICENSE_CANCELLED"{s.clear()}else{s.write("session.json",state)}};return nil,e} envelope:=canonical(response["license"]);result,e:=s.verify(envelope,"online",state.Checkpoint,id);if e!=nil{state.Envelope=nil;s.write("session.json",state);return nil,e};state.Envelope=&envelope;state.Checkpoint=time.Now().UTC().Format(time.RFC3339Nano);return result,s.write("session.json",state) } func execute()(code int){code=2;defer func(){if recover()!=nil{fmt.Fprintln(os.Stderr,"Check application settings and private storage. Contact your software provider if the problem continues.")}}() if len(os.Args)!=2||!regexp.MustCompile(`^(status|activate|check|validate|deactivate)$`).MatchString(os.Args[1]){panic("action")};s,e:=openSession();if e!=nil{panic(e)};defer s.close();result,e:=s.run(os.Args[1]);if e!=nil{var denied refusal;var offline unreachable if !errors.As(e,&denied)&&!errors.As(e,&offline){panic(e)};if denied.code==""{denied.code="NETWORK_ERROR"};result=success("local",denied.requestID);result["valid"]=false;result["error_code"]=denied.code;result["message"]="Your license could not be confirmed. Connect and try again, or contact your software provider.";code=1}else{code=0} raw,e:=json.Marshal(result);if e!=nil{panic(e)};fmt.Println(string(raw));return code} func main(){os.Exit(execute())} ``` ```text module example.invalid/kdpika-rest-session go 1.22 ``` 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 CLI or backend, without third-party Go runtime modules. Build for each target and validate its TLS roots/ACLs. Its reusable signature.go is derived from the complete P5 verifier with only the original CLI main and unused os import removed. An exclusive-create lock refuses concurrent writers; reconcile a stale lock after confirming its process ended. 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) # Rust REST client Canonical: https://licensing.kdpika.com/docs/v1/sdks/rust --- title: "Rust REST client" description: "Build a complete Rust Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating." slug: "/docs/v1/sdks/rust" section: "SDKs and languages" order: 57 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 Rust 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 Rust1.82+, pinned Cargo.toml/Cargo.lock dependencies. Native CLI or backend. fs2 provides an OS-held lock on a stable file. Pinned ureq uses TLS with redirects disabled; chrono provides UTC timestamp parsing and uuid supplies random [installation IDs](/docs/v1/start/glossary#installation-id). signature.rs keeps the complete P5 verifier and exposes only validated JSON/raw envelope accessors. No official Rust SDK or registry publication is implied. ## 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-rust-rest-example-1.0.0.zip](/docs/downloads/kdpika-rust-rest-example-1.0.0.zip) · 14745 bytes · version 1.0.0 SHA-256: 8df02e42e7b2fcef034a50326543f29ca8fadea8182cc001a643bfcc74545d44 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/rust. 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 cargo build --locked ``` 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 ```rust // Complete Sandbox REST session. Rust1.82+, exact Cargo.toml/Cargo.lock. // Run cargo run -- status|activate|check|validate|deactivate; safe JSON only. // signature.rs is the unchanged P5 verifier body, without its original CLI main. mod signature; use chrono::{DateTime,Duration,Utc}; use fs2::FileExt; use serde::{Deserialize,Serialize}; use serde_json::{Value,json}; use sha2::{Digest,Sha256}; use std::{env,fs::{self,File,OpenOptions},io::{Read,Write},path::{Path,PathBuf},time::Duration as Timeout}; enum Error {Setup,Network,Denied(String,Option)} type Result = std::result::Result; fn denied(code:&str)->Error {Error::Denied(code.into(),None)} fn config(name:&str)->Result{let value=env::var(name).map_err(|_|Error::Setup)?;if value.is_empty()||value.contains(['\r','\n']){return Err(Error::Setup)};Ok(value)} fn hash(value:&str)->String{format!("{:x}",Sha256::digest(value.as_bytes()))} fn safe_identifier(value:&str,max:usize)->bool{!value.is_empty()&&value.len()<=max&&value.bytes().all(|c|c.is_ascii_alphanumeric()||c==b'_'||c==b'-')} fn read(path:&Path)->Result>{let stat=fs::symlink_metadata(path).map_err(|_|Error::Setup)?;if !stat.is_file()||stat.file_type().is_symlink()||stat.len()>1048576{return Err(Error::Setup)};fs::read(path).map_err(|_|Error::Setup)} fn object(raw:&[u8])->Result{signature::validate_json(raw).map_err(|_|denied("INVALID_RESPONSE"))?;let v:Value=serde_json::from_slice(raw).map_err(|_|denied("INVALID_RESPONSE"))?;if !v.is_object(){return Err(denied("INVALID_RESPONSE"))};Ok(v)} fn timestamp(value:&Value)->Result>{DateTime::parse_from_rfc3339(value.as_str().ok_or_else(||denied("INVALID_TIME"))?).map(|v|v.with_timezone(&Utc)).map_err(|_|denied("INVALID_TIME"))} fn permission(path:&Path,mode:u32)->Result<()>{ #[cfg(unix)]{use std::os::unix::fs::PermissionsExt;fs::set_permissions(path,fs::Permissions::from_mode(mode)).map_err(|_|Error::Setup)?;} #[cfg(not(unix))]{let _=(path,mode);} Ok(()) } #[derive(Serialize,Deserialize)] struct Stored {token:String,envelope:Option,checkpoint:String} struct Session {app:String,base:String,directory:PathBuf,installation:String,key:Vec,lock:File,agent:ureq::Agent} impl Session { fn open()->Result{ let app=config("KDP_APP_ID")?;if !safe_identifier(&app,160){return Err(Error::Setup)} 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).map_err(|_|Error::Setup)?; 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(Error::Setup)} let root=PathBuf::from(config("KDP_STATE_DIR")?);let keyfile=PathBuf::from(config("KDP_TRUSTED_KEY_FILE")?);if !root.is_absolute()||!keyfile.is_absolute(){return Err(Error::Setup)};let key=read(&keyfile)?;let public=object(&key)?;if public["algorithm"]!="Ed25519"||public["encoding"]!="base64url"{return Err(Error::Setup)} fs::create_dir_all(&root).map_err(|_|Error::Setup)?;if fs::symlink_metadata(&root).map_err(|_|Error::Setup)?.file_type().is_symlink(){return Err(Error::Setup)} let directory=root.join(&hash(&format!("{base}|{app}|SANDBOX|rust-rest-v1"))[..20]);fs::create_dir_all(&directory).map_err(|_|Error::Setup)?; if fs::symlink_metadata(&directory).map_err(|_|Error::Setup)?.file_type().is_symlink(){return Err(Error::Setup)};permission(&directory,0o700)?; let lockfile=directory.join(".lock");if let Ok(stat)=fs::symlink_metadata(&lockfile){if !stat.is_file()||stat.file_type().is_symlink(){return Err(Error::Setup)}} let lock=OpenOptions::new().create(true).truncate(false).read(true).write(true).open(&lockfile).map_err(|_|Error::Setup)?;permission(&lockfile,0o600)?;lock.try_lock_exclusive().map_err(|_|Error::Setup)?; let mut result=Self {app,base,directory,installation:String::new(),key,lock,agent:ureq::AgentBuilder::new().timeout(Timeout::from_secs(15)).redirects(0).build()}; let identity=result.directory.join("installation.json");if !identity.exists(){result.write("installation.json",&json!({"installation_id":format!("lmp-install-{}",uuid::Uuid::new_v4())}))?} let value=object(&read(&identity)?)?;result.installation=value["installation_id"].as_str().ok_or(Error::Setup)?.to_string(); if !result.installation.starts_with("lmp-install-")||result.installation.len()!=48||!safe_identifier(&result.installation,48){return Err(Error::Setup)};Ok(result) } fn write(&self,name:&str,value:&T)->Result<()>{let temporary=self.directory.join(format!("write-{}",uuid::Uuid::new_v4()));let mut file=OpenOptions::new().create_new(true).write(true).open(&temporary).map_err(|_|Error::Setup)?;permission(&temporary,0o600)?;file.write_all(&serde_json::to_vec(value).map_err(|_|Error::Setup)?).map_err(|_|Error::Setup)?;file.sync_all().map_err(|_|Error::Setup)?;drop(file);fs::rename(&temporary,self.directory.join(name)).map_err(|_|Error::Setup)} fn state(&self)->Result>{let file=self.directory.join("session.json");if !file.exists(){return Ok(None)};let raw=read(&file)?;object(&raw)?;let state:Stored=serde_json::from_slice(&raw).map_err(|_|Error::Setup)?;if !state.token.starts_with("act_")||!safe_identifier(&state.token,256){return Err(Error::Setup)};timestamp(&json!(state.checkpoint))?;Ok(Some(state))} fn clear(&self)->Result<()>{match fs::remove_file(self.directory.join("session.json")){Ok(())=>Ok(()),Err(e) if e.kind()==std::io::ErrorKind::NotFound=>Ok(()),Err(_)=>Err(Error::Setup)}} fn request(&self,endpoint:&str,body:&Value,token:Option<&str>)->Result<(Vec,Value,Option)>{ let mut request=self.agent.post(&(self.base.clone()+endpoint)).set("Accept","application/json").set("Content-Type","application/json").set("User-Agent","KDPika-REST-Rust-example/1.0"); request=if let Some(token)=token{request.set("Authorization",&format!("Bearer {token}")).set("X-Installation-ID",&self.installation)}else{request.set("X-App-ID",&self.app).set("X-Environment","SANDBOX")}; let response=match request.send_string(&body.to_string()){Ok(r)=>r,Err(ureq::Error::Status(_,r))=>r,Err(_)=>return Err(Error::Network)};let status=response.status();let mut raw=Vec::new();response.into_reader().take(1048577).read_to_end(&mut raw).map_err(|_|Error::Network)?; if raw.len()>1048576{return Err(denied("INVALID_RESPONSE"))};let value=object(&raw)?;let id=value["request_id"].as_str().filter(|id|id.starts_with("req_")&&safe_identifier(id,124)).map(String::from); if !(200..300).contains(&status)||value["success"]!=true {let code=value["error"]["code"].as_str().filter(|v|v.len()<=64&&v.bytes().all(|c|c.is_ascii_uppercase()||c.is_ascii_digit()||c==b'_')).unwrap_or("HTTP_ERROR");return Err(Error::Denied(code.into(),id))};Ok((raw,value,id)) } fn verify(&self,envelope:&str,source:&str,checkpoint:Option<&str>,id:Option)->Result{ if !signature::verify_envelope(&self.key,envelope.as_bytes()){return Err(Error::Denied("SIGNATURE_INVALID".into(),id))};let env=object(envelope.as_bytes())?;let p=&env["payload"]; if p["product_id"]!=self.app||p["environment"]!="sandbox"||p["device"]["installation_hash"]!=hash(&self.installation){return Err(denied("BINDING_MISMATCH"))} let server=timestamp(&p["server_time"])?;let next=timestamp(&p["next_validation_at"])?;let grace=timestamp(&p["offline_grace_until"])?;if nextserver.max(timestamp(&json!(c))?),None=>server};let now=Utc::now(); if now+Duration::seconds(300)=timestamp(&p["expires_at"])?{return Err(denied("LICENSE_EXPIRED"))};if source!="online"&&now>grace{return Err(denied("OFFLINE_GRACE_EXPIRED"))} if !p["features"].is_object(){return Err(denied("INVALID_RESPONSE"))};let mut value=accepted(source,id);value["signature_valid"]=json!(true);value["pdf_export_allowed"]=json!(p["features"]["pdf_export"]==true);if let Some(count)=p["features"]["seat_count"].as_u64(){if count<=9007199254740991{value["seat_count"]=json!(count)}};Ok(value) } fn run(&self,action:&str)->Result{ let saved=self.state()?;if action=="status"{return Ok(json!({"activated":saved.is_some(),"installation_hash":hash(&self.installation),"storage_security":"permission-protected-file"}))} if action=="activate"{let license=config("KDP_LICENSE_KEY")?;if !license.starts_with("TEST"){return Err(Error::Setup)};if saved.is_some(){return Err(denied("ALREADY_ACTIVATED"))};let mut body=json!({"license_key":license,"device_id":self.installation,"device_name":"Rust REST example","platform":env::consts::OS,"app_version":"1.0.0"});if env::var("KDP_CUSTOMER_EMAIL").is_ok_and(|v|!v.is_empty()){body["email"]=json!(config("KDP_CUSTOMER_EMAIL")?)} let(raw,reply,id)=self.request("/licenses/activate",&body,None)?;let token=reply["activation_token"].as_str().ok_or_else(||denied("INVALID_RESPONSE"))?;if !token.starts_with("act_")||!safe_identifier(token,256){return Err(denied("INVALID_RESPONSE"))} let envelope=signature::envelope_from_response(&raw).map_err(|_|denied("INVALID_RESPONSE"))?;match self.verify(&envelope,"online",None,id){Ok(value)=>{self.write("session.json",&Stored{token:token.into(),envelope:Some(envelope),checkpoint:Utc::now().to_rfc3339()})?;return Ok(value)},Err(error)=>{let _=self.request("/licenses/deactivate",&json!({}),Some(token));return Err(error)}}} let mut state=saved.ok_or_else(||denied("NOT_ACTIVATED"))?;if action=="deactivate"{let(_,reply,id)=self.request("/licenses/deactivate",&json!({}),Some(&state.token))?;if reply["deactivated"]!=true{return Err(denied("INVALID_RESPONSE"))};self.clear()?;let mut value=accepted("online",id);value["message"]=json!("This installation has been deactivated.");return Ok(value)} if action=="check"{if let Some(envelope)=&state.envelope{match self.verify(envelope,"cache",Some(&state.checkpoint),None){Ok(value)=>{let p=object(envelope.as_bytes())?;if Utc::now(){state.envelope=None;self.write("session.json",&state)?}}}} let answer=self.request("/licenses/validate",&json!({}),Some(&state.token));let value=match answer { Ok((raw,_,id))=>{let envelope=signature::envelope_from_response(&raw).map_err(|_|denied("INVALID_RESPONSE"))?;match self.verify(&envelope,"online",Some(&state.checkpoint),id){Ok(value)=>{state.envelope=Some(envelope);value},Err(error)=>{state.envelope=None;self.write("session.json",&state)?;return Err(error)}}}, Err(Error::Network)=>{let envelope=state.envelope.as_ref().ok_or_else(||denied("OFFLINE_CACHE_MISSING"))?;self.verify(envelope,"offline",Some(&state.checkpoint),None)?}, Err(Error::Denied(code,id))=>{if ["INVALID_TOKEN","DEVICE_MISMATCH","LICENSE_REVOKED","LICENSE_EXPIRED","LICENSE_CANCELLED"].contains(&code.as_str()){self.clear()?}else{state.envelope=None;self.write("session.json",&state)?};return Err(Error::Denied(code,id))},Err(error)=>return Err(error)}; state.checkpoint=Utc::now().to_rfc3339();self.write("session.json",&state)?;Ok(value) } } impl Drop for Session {fn drop(&mut self){let _=FileExt::unlock(&self.lock);}} fn accepted(source:&str,id:Option)->Value{json!({"valid":true,"source":source,"signature_valid":false,"error_code":null,"request_id":id,"pdf_export_allowed":false,"seat_count":null,"message":"Your license is ready."})} fn execute()->Result{let args:Vec=env::args().collect();if args.len()!=2||!["status","activate","check","validate","deactivate"].contains(&args[1].as_str()){return Err(Error::Setup)};let client=Session::open()?;match client.run(&args[1]){Ok(value)=>{println!("{value}");Ok(0)},Err(error)=>{let(code,id)=match error{Error::Denied(c,id)=>(c,id),Error::Network=>("NETWORK_ERROR".into(),None),Error::Setup=>return Err(Error::Setup)};let mut value=accepted("local",id);value["valid"]=json!(false);value["error_code"]=json!(code);value["message"]=json!("Your license could not be confirmed. Connect and try again, or contact your software provider.");println!("{value}");Ok(1)}}} fn main(){let code=match execute(){Ok(code)=>code,Err(_)=>{eprintln!("Check application settings and private storage. Contact your software provider if the problem continues.");2}};std::process::exit(code)} ``` ```toml [package] name = "kdpika-rust-rest-example" version = "1.0.0" edition = "2021" rust-version = "1.82" publish = false [dependencies] base64 = "=0.22.1" base64ct = "=1.6.0" ed25519-dalek = "=2.2.0" serde = { version = "=1.0.228", features = ["derive"] } serde_json = { version = "=1.0.145", features = ["raw_value"] } sha2 = "=0.10.9" zeroize = "=1.8.1" ureq = { version = "=2.12.1", default-features = false, features = ["tls"] } url = "=2.5.2" chrono = { version = "=0.4.38", default-features = false, features = ["clock", "std"] } uuid = { version = "=1.11.0", features = ["v4"] } fs2 = "=0.4.3" ``` 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 ./target/debug/kdpika-rust-rest-example 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 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 CLI or backend. fs2 provides an OS-held lock on a stable file. Pinned ureq uses TLS with redirects disabled; chrono provides UTC timestamp parsing and uuid supplies random installation IDs. signature.rs keeps the complete P5 verifier and exposes only validated JSON/raw envelope accessors. No official Rust SDK or registry publication is implied. 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) # Ruby REST client Canonical: https://licensing.kdpika.com/docs/v1/sdks/ruby --- title: "Ruby REST client" description: "Build a complete Ruby Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating." slug: "/docs/v1/sdks/ruby" section: "SDKs and languages" order: 58 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 Ruby 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 Ruby2.6+, libsodium1.0.20, standard-library Net::HTTP/Fiddle. Trusted Ruby CLI or backend. Install the native libsodium library and set SODIUM_LIBRARY to its absolute platform path when library discovery does not find it. No Rails plugin is implied. The Net::HTTP client disables automatic retries and redirects and verifies TLS peers. ## 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-ruby-rest-example-1.0.0.zip](/docs/downloads/kdpika-ruby-rest-example-1.0.0.zip) · 7082 bytes · version 1.0.0 SHA-256: aa018d402159802d6189e7fb9cf21be95581c206acbca80a4a37cf18d6a022f3 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/ruby. 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 ruby -c session.rb ``` 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 ```ruby # Complete Sandbox REST CLI. Ruby2.6+, libsodium1.0.20; stdlib HTTP/TLS. # Run ruby session.rb status|activate|check|validate|deactivate. Safe decisions. # Requires unchanged ../../security/ruby/verify.rb and a trusted public key file. require 'net/http' require 'uri' require 'time' require 'securerandom' require 'fileutils' require_relative '../../security/ruby/verify' class Refusal < StandardError attr_reader :code, :request_id def initialize(code, id = nil); super(code); @code = code; @request_id = id; end end class Unreachable < StandardError; end class Session def required(name) value = ENV.fetch(name) raise 'Invalid configuration' if value.empty? || value.match?(/[\r\n]/) value end def read(file) stat = File.lstat(file) raise 'Unsafe file' unless stat.file? && !stat.symlink? && stat.size <= 1_048_576 File.binread(file) end def object(raw) value = RawJson.new(raw.dup).parse raise Refusal.new('INVALID_RESPONSE') unless value.is_a?(Hash) value end def initialize @app = required('KDP_APP_ID'); raise 'App ID' unless /\A[A-Za-z0-9_-]{3,160}\z/.match?(@app) uri = URI(ENV.fetch('KDP_BASE_URL', 'https://licensing.kdpika.com/api/v1').sub(%r{/+\z}, '')) local = uri.scheme == 'http' && ['127.0.0.1', 'localhost'].include?(uri.host) && ENV['KDP_ALLOW_LOOPBACK'] == '1' raise 'Unsafe URL' unless uri.host && (uri.scheme == 'https' || local) && !uri.userinfo && !uri.query && !uri.fragment && uri.path == '/api/v1' @base = uri.to_s root = required('KDP_STATE_DIR'); key = required('KDP_TRUSTED_KEY_FILE') raise 'Absolute paths required' unless File.absolute_path(root) == root && File.absolute_path(key) == key @trusted = read(key); public_key = object(@trusted) raise 'Trusted key' unless public_key['algorithm'] == 'Ed25519' && public_key['encoding'] == 'base64url' FileUtils.mkdir_p(root, mode: 0o700); raise 'Root symlink' if File.symlink?(root) @dir = File.join(root, Digest::SHA256.hexdigest([@base, @app, 'SANDBOX', 'ruby-rest-v1'].join('|'))[0, 20]) FileUtils.mkdir_p(@dir, mode: 0o700); raise 'Namespace symlink' if File.symlink?(@dir) File.chmod(0o700, @dir) @lock = File.open(File.join(@dir, '.lock'), File::WRONLY | File::CREAT | File::EXCL, 0o600) begin identity = File.join(@dir, 'installation.json') write('installation.json', {'installation_id' => 'lmp-install-' + SecureRandom.uuid}) unless File.exist?(identity) || File.symlink?(identity) @installation = object(read(identity)).fetch('installation_id') raise 'Invalid identity; do not reset automatically' unless /\Almp-install-[a-f0-9-]{36}\z/.match?(@installation) rescue StandardError close; raise end end def close return unless @lock @lock.close; File.unlink(File.join(@dir, '.lock')); @lock = nil end def write(name, value) temporary = File.join(@dir, 'write-' + SecureRandom.hex(12)) File.open(temporary, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(JSON.generate(value)); file.flush; file.fsync } File.rename(temporary, File.join(@dir, name)) end def state file = File.join(@dir, 'session.json'); return nil unless File.exist?(file) || File.symlink?(file) raw = read(file); object(raw); value = JSON.parse(raw) raise 'Invalid state' unless value['token'].is_a?(String) && /\Aact_[A-Za-z0-9_-]+\z/.match?(value['token']) && (value['envelope'].nil? || value['envelope'].is_a?(String)) && value['checkpoint'].is_a?(String) timestamp(value['checkpoint']); value end def clear file = File.join(@dir, 'session.json'); File.unlink(file) if File.exist?(file) || File.symlink?(file) end def timestamp(value) raise Refusal.new('INVALID_TIME') unless value.is_a?(String) && /\A\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)\z/.match?(value) Time.iso8601(value) rescue ArgumentError raise Refusal.new('INVALID_TIME') end def request(endpoint, body, token = nil) uri = URI(@base + endpoint); request = Net::HTTP::Post.new(uri.request_uri) request['Accept'] = 'application/json'; request['Content-Type'] = 'application/json'; request['User-Agent'] = 'KDPika-REST-Ruby-example/1.0' if token request['Authorization'] = 'Bearer ' + token; request['X-Installation-ID'] = @installation else request['X-App-ID'] = @app; request['X-Environment'] = 'SANDBOX' end request.body = JSON.generate(body); raw = ''.b; response = nil begin # Explicit nil proxy prevents accidental forwarding through environment proxies. http = Net::HTTP.new(uri.host, uri.port, nil); http.use_ssl = uri.scheme == 'https'; http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.open_timeout = 5; http.read_timeout = 15; http.max_retries = 0 http.start { |connection| connection.request(request) { |reply| response = reply; reply.read_body { |part| raw << part; raise Refusal.new('INVALID_RESPONSE') if raw.bytesize > 1_048_576 } } } rescue Refusal raise rescue StandardError raise Unreachable end begin; value = object(raw); rescue StandardError; raise Refusal.new('INVALID_RESPONSE'); end id = value['request_id']; id = nil unless id.is_a?(String) && /\Areq_[A-Za-z0-9_-]{1,120}\z/.match?(id) unless response.code.to_i.between?(200, 299) && value['success'] == true code = value.dig('error', 'code') rescue nil code = 'HTTP_ERROR' unless code.is_a?(String) && /\A[A-Z0-9_]{1,64}\z/.match?(code) raise Refusal.new(code, id) end [value, id] end def accepted(source, id = nil) {'valid' => true, 'source' => source, 'signature_valid' => false, 'error_code' => nil, 'request_id' => id, 'pdf_export_allowed' => false, 'seat_count' => nil, 'message' => 'Your license is ready.'} end def verify(envelope, source, checkpoint, id = nil) raise Refusal.new('SIGNATURE_INVALID', id) unless verify_envelope(@trusted.dup, envelope.dup) payload = JSON.parse(envelope).fetch('payload'); now = Time.now unless payload['product_id'] == @app && payload['environment'] == 'sandbox' && payload.dig('device', 'installation_hash') == Digest::SHA256.hexdigest(@installation) raise Refusal.new('BINDING_MISMATCH', id) end server = timestamp(payload['server_time']); next_time = timestamp(payload['next_validation_at']); grace = timestamp(payload['offline_grace_until']) raise Refusal.new('INVALID_TIME', id) if next_time < server || grace < server latest = checkpoint ? [server, timestamp(checkpoint)].max : server raise Refusal.new('CLOCK_ROLLBACK_DETECTED', id) if now + 300 < latest raise Refusal.new('LICENSE_NOT_ACTIVE', id) unless payload['status'] == 'active' raise Refusal.new('LICENSE_NOT_STARTED', id) if payload['starts_at'] && now < timestamp(payload['starts_at']) raise Refusal.new('LICENSE_EXPIRED', id) if payload['expires_at'] && now >= timestamp(payload['expires_at']) raise Refusal.new('OFFLINE_GRACE_EXPIRED', id) if source != 'online' && now > grace features = payload.fetch('features'); raise Refusal.new('INVALID_RESPONSE') unless features.is_a?(Hash) value = accepted(source, id); value['signature_valid'] = true; value['pdf_export_allowed'] = features['pdf_export'] == true seat = features['seat_count']; value['seat_count'] = seat if seat.is_a?(Integer) && seat.between?(0, 9_007_199_254_740_991) value end def run(action) saved = state return {'activated' => !saved.nil?, 'installation_hash' => Digest::SHA256.hexdigest(@installation), 'storage_security' => 'permission-protected-file'} if action == 'status' if action == 'activate' license = required('KDP_LICENSE_KEY'); raise 'Sandbox key required' unless license.start_with?('TEST') raise Refusal.new('ALREADY_ACTIVATED') if saved body = {'license_key' => license, 'device_id' => @installation, 'device_name' => 'Ruby REST example', 'platform' => RUBY_PLATFORM, 'app_version' => '1.0.0'} body['email'] = required('KDP_CUSTOMER_EMAIL') if ENV['KDP_CUSTOMER_EMAIL'] && !ENV['KDP_CUSTOMER_EMAIL'].empty? reply, id = request('/licenses/activate', body); token = reply['activation_token'] raise Refusal.new('INVALID_RESPONSE', id) unless token.is_a?(String) && /\Aact_[A-Za-z0-9_-]+\z/.match?(token) begin envelope = canonical(reply.fetch('license')); result = verify(envelope, 'online', nil, id) write('session.json', {'token' => token, 'envelope' => envelope, 'checkpoint' => Time.now.utc.iso8601(6)}); return result rescue StandardError begin; request('/licenses/deactivate', {}, token); rescue StandardError; end raise end end raise Refusal.new('NOT_ACTIVATED') unless saved if action == 'deactivate' reply, id = request('/licenses/deactivate', {}, saved['token']); raise Refusal.new('INVALID_RESPONSE', id) unless reply['deactivated'] == true clear; value = accepted('online', id); value['message'] = 'This installation has been deactivated.'; return value end if action == 'check' && saved['envelope'] begin value = verify(saved['envelope'], 'cache', saved['checkpoint']) if Time.now < timestamp(JSON.parse(saved['envelope']).dig('payload', 'next_validation_at')) saved['checkpoint'] = Time.now.utc.iso8601(6); write('session.json', saved); return value end rescue Refusal saved['envelope'] = nil; write('session.json', saved) end end begin reply, id = request('/licenses/validate', {}, saved['token']); envelope = canonical(reply.fetch('license')) value = verify(envelope, 'online', saved['checkpoint'], id); saved['envelope'] = envelope rescue Unreachable raise Refusal.new('OFFLINE_CACHE_MISSING') unless saved['envelope'] value = verify(saved['envelope'], 'offline', saved['checkpoint']) rescue Refusal => error if ['INVALID_TOKEN', 'DEVICE_MISMATCH', 'LICENSE_REVOKED', 'LICENSE_EXPIRED', 'LICENSE_CANCELLED'].include?(error.code) clear else saved['envelope'] = nil; write('session.json', saved) end raise end saved['checkpoint'] = Time.now.utc.iso8601(6); write('session.json', saved); value end end if $PROGRAM_NAME == __FILE__ client = nil; code = 2 begin raise 'Choose an action' unless ARGV.length == 1 && ['status', 'activate', 'check', 'validate', 'deactivate'].include?(ARGV[0]) client = Session.new begin puts JSON.generate(client.run(ARGV[0])); code = 0 rescue Refusal, Unreachable => error value = client.accepted('local', error.is_a?(Refusal) ? error.request_id : nil) value['valid'] = false; value['error_code'] = error.is_a?(Refusal) ? error.code : 'NETWORK_ERROR' value['message'] = 'Your license could not be confirmed. Connect and try again, or contact your software provider.' puts JSON.generate(value); code = 1 end rescue StandardError warn 'Check application settings and private storage. Contact your software provider if the problem continues.' ensure client.close if client end exit code end ``` 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 ruby session.rb 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 Trusted Ruby CLI or backend. Install the native libsodium library and set SODIUM_LIBRARY to its absolute platform path when library discovery does not find it. No Rails plugin is implied. The Net::HTTP client disables automatic retries and redirects and verifies TLS peers. 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) # C REST client Canonical: https://licensing.kdpika.com/docs/v1/sdks/c --- title: "C REST client" description: "Build a complete C Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating." slug: "/docs/v1/sdks/c" section: "SDKs and languages" order: 59 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 C 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 C11/POSIX, libcurl, yyjson0.13.0, libsodium1.0.20. POSIX C core using owner-only files, flock, atomic writes, libcurl TLS verification and exact yyjson numeric lexemes. The script defaults SODIUM_PREFIX to /usr/local; set your actual installed prefix. Its dependency downloads are pinned by SHA-256. macOS is tested; Linux requires compatible POSIX APIs/libcurl; native Windows needs a deliberate storage/time/locking port. ## 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-c-rest-example-1.0.0.zip](/docs/downloads/kdpika-c-rest-example-1.0.0.zip) · 11152 bytes · version 1.0.0 SHA-256: 720365b7b1957b62e1ea7ff86566340e792a14bb568a247788a40f717d0aa4d5 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/c. 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 SODIUM_PREFIX=/opt/homebrew/opt/libsodium 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 ```c /* C11 entry point. Link the complete native/session.c and yyjson.c. * Purpose: real Sandbox session; output safe JSON. No credential arguments. * Run ./session status|activate|check|validate|deactivate. */ int kdpika_session_main(int argc,char **argv); int main(int argc,char **argv){return kdpika_session_main(argc,argv);} ``` ```c /* Complete Sandbox REST session core; C11/POSIX, libcurl, yyjson0.13.0, * libsodium1.0.20. Linked by both C and C++ entry points. Never logs secrets. * Run status|activate|check|validate|deactivate with documented KDP_* config. */ #define _DARWIN_C_SOURCE #define _GNU_SOURCE #include "../../security/c/canonical.h" #include #include #include #include #include #include #include #include typedef struct { char *app,*base,*directory,*installation,*trusted;size_t trusted_size;int lock;const char *error;char request_id[128]; } Session; typedef struct { char *data;size_t size;int oversized; } Buffer; static int safe_text(const char *value,size_t limit) {if(!value||!*value||strlen(value)>limit)return 0;for(const char *p=value;*p;p++)if(*p=='\r'||*p=='\n')return 0;return 1;} static int identifier(const char *value,size_t limit) {if(!safe_text(value,limit))return 0;for(const char *p=value;*p;p++)if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='_'||*p=='-'))return 0;return 1;} static const char *string(yyjson_val *value,const char *name){return yyjson_get_str(yyjson_obj_get(value,name));} static char *join(const char *a,const char *b){size_t size=strlen(a)+strlen(b)+1;char *v=(char*)malloc(size);if(v)snprintf(v,size,"%s%s",a,b);return v;} static void hash(const char *value,char out[65]) {unsigned char digest[32];crypto_hash_sha256(digest,(const unsigned char*)value,strlen(value));sodium_bin2hex(out,65,digest,32);} static char *read_private(const char *file,size_t *size) { struct stat stat;int fd=open(file,O_RDONLY|O_NOFOLLOW);if(fd<0)return NULL; if(fstat(fd,&stat)||!S_ISREG(stat.st_mode)||stat.st_size>1048576||stat.st_size<0){close(fd);return NULL;} char *raw=(char*)malloc((size_t)stat.st_size+1);if(!raw){close(fd);return NULL;}size_t n=0; while(n<(size_t)stat.st_size){ssize_t got=read(fd,raw+n,(size_t)stat.st_size-n);if(got<=0){free(raw);close(fd);return NULL;}n+=(size_t)got;}close(fd);raw[n]=0;*size=n;return raw; } static int write_atomic(Session *s,const char *name,const char *raw) { char *template=join(s->directory,"/write-XXXXXX");if(!template)return 0;int fd=mkstemp(template);if(fd<0){free(template);return 0;}int ok=fchmod(fd,0600)==0;size_t n=0,len=strlen(raw); while(ok&&ndirectory,name);if(ok)ok=target&&rename(template,target)==0;free(target);free(template);return ok; } static char *dump(yyjson_val *value){Output result={NULL,0,0,1};canonical(&result,value,0);if(!result.ok){free(result.data);return NULL;}return result.data;} static int directory(const char *path) {struct stat st;if(mkdir(path,0700)&&errno!=EEXIST)return 0;if(lstat(path,&st)||!S_ISDIR(st.st_mode)||S_ISLNK(st.st_mode))return 0;return chmod(path,0700)==0;} static char *now_string(void){time_t now=time(NULL);struct tm utc;if(!gmtime_r(&now,&utc))return NULL;char text[40];if(!strftime(text,sizeof text,"%Y-%m-%dT%H:%M:%SZ",&utc))return NULL;return strdup(text);} static int timestamp(const char *text,double *result) { if(!text)return 0;struct tm tm={0};char *tail=strptime(text,"%Y-%m-%dT%H:%M:%S",&tm);if(!tail)return 0;double fraction=0; if(*tail=='.'){char *end;fraction=strtod(tail,&end);if(end==tail)return 0;tail=end;} int offset=0;if(*tail=='Z')tail++;else if(*tail=='+'||*tail=='-'){int sign=*tail++=='+'?1:-1;int hours,minutes,used=0;if(sscanf(tail,"%2d:%2d%n",&hours,&minutes,&used)!=2||used!=5||hours>23||minutes>59)return 0;offset=sign*(hours*3600+minutes*60);tail+=used;}else return 0; if(*tail)return 0;*result=(double)timegm(&tm)+fraction-offset;return 1; } static size_t receive(char *data,size_t size,size_t count,void *context) { Buffer *b=(Buffer*)context;if(size&&count>SIZE_MAX/size)return 0;size_t n=size*count;if(n>1048576-b->size){b->oversized=1;return 0;}char *next=(char*)realloc(b->data,b->size+n+1);if(!next)return 0;b->data=next;memcpy(next+b->size,data,n);b->size+=n;next[b->size]=0;return n; } static yyjson_doc *request(Session *s,const char *endpoint,const char *body,const char *token) { s->error=NULL;s->request_id[0]=0;CURL *curl=curl_easy_init();if(!curl){s->error="NETWORK_ERROR";return NULL;}char *url=join(s->base,endpoint);Buffer buffer={NULL,0,0};struct curl_slist *headers=NULL; headers=curl_slist_append(headers,"Accept: application/json");headers=curl_slist_append(headers,"Content-Type: application/json");char *auth=join(token?"Authorization: Bearer ":"X-App-ID: ",token?token:s->app);headers=curl_slist_append(headers,auth);free(auth); if(token){char *installation=join("X-Installation-ID: ",s->installation);headers=curl_slist_append(headers,installation);free(installation);}else headers=curl_slist_append(headers,"X-Environment: SANDBOX"); curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body);curl_easy_setopt(curl,CURLOPT_POSTFIELDSIZE,(long)strlen(body));curl_easy_setopt(curl,CURLOPT_TIMEOUT,15L);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L); curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);curl_easy_setopt(curl,CURLOPT_PROTOCOLS_STR,"http,https");curl_easy_setopt(curl,CURLOPT_USERAGENT,"KDPika-REST-native-example/1.0");curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&buffer); CURLcode code=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);curl_slist_free_all(headers);free(url); if(code!=CURLE_OK){s->error=buffer.oversized?"INVALID_RESPONSE":"NETWORK_ERROR";free(buffer.data);return NULL;} yyjson_doc *doc=buffer.data?parse(buffer.data,buffer.size):NULL;free(buffer.data);if(!doc){s->error="INVALID_RESPONSE";return NULL;}yyjson_val *root=yyjson_doc_get_root(doc);const char *id=string(root,"request_id");if(id&&strncmp(id,"req_",4)==0&&identifier(id,124))snprintf(s->request_id,sizeof s->request_id,"%s",id); if(status<200||status>=300||!yyjson_is_true(yyjson_obj_get(root,"success"))){const char *error=string(yyjson_obj_get(root,"error"),"code");s->error="HTTP_ERROR";if(error&&identifier(error,64)){static _Thread_local char copied[65];snprintf(copied,sizeof copied,"%s",error);s->error=copied;}yyjson_doc_free(doc);return NULL;}return doc; } static int save(Session *s,const char *token,const char *envelope,const char *checkpoint) { yyjson_mut_doc *doc=yyjson_mut_doc_new(NULL);yyjson_mut_val *root=yyjson_mut_obj(doc);yyjson_mut_doc_set_root(doc,root); yyjson_mut_obj_add_str(doc,root,"token",token);if(envelope)yyjson_mut_obj_add_str(doc,root,"envelope",envelope);else yyjson_mut_obj_add_null(doc,root,"envelope");yyjson_mut_obj_add_str(doc,root,"checkpoint",checkpoint); char *raw=yyjson_mut_write(doc,0,NULL);int ok=raw&&write_atomic(s,"/session.json",raw);free(raw);yyjson_mut_doc_free(doc);return ok; } static void clear(Session *s) {char *file=join(s->directory,"/session.json");if(file){unlink(file);free(file);}} static int verify(Session *s,const char *envelope,const char *source,const char *checkpoint,int *enabled,long long *seats) { s->error=NULL;*enabled=0;*seats=-1;if(!verify_envelope(s->trusted,s->trusted_size,envelope,strlen(envelope))){s->error="SIGNATURE_INVALID";return 0;} yyjson_doc *doc=parse(envelope,strlen(envelope));if(!doc){s->error="INVALID_RESPONSE";return 0;}yyjson_val *p=yyjson_obj_get(yyjson_doc_get_root(doc),"payload");char identity[65];hash(s->installation,identity); if(!equals(p,"product_id",s->app)||!equals(p,"environment","sandbox")||!equals(yyjson_obj_get(p,"device"),"installation_hash",identity))s->error="BINDING_MISMATCH"; double server=0,next=0,grace=0,latest=0,expiry=0,start=0,now=(double)time(NULL); if(!s->error&&(!timestamp(string(p,"server_time"),&server)||!timestamp(string(p,"next_validation_at"),&next)||!timestamp(string(p,"offline_grace_until"),&grace)||nexterror="INVALID_TIME"; latest=server;if(!s->error&&checkpoint){double previous;if(!timestamp(checkpoint,&previous))s->error="INVALID_TIME";else if(previous>latest)latest=previous;} if(!s->error&&now+300error="CLOCK_ROLLBACK_DETECTED";if(!s->error&&!equals(p,"status","active"))s->error="LICENSE_NOT_ACTIVE"; yyjson_val *begin=yyjson_obj_get(p,"starts_at"),*end=yyjson_obj_get(p,"expires_at"); if(!s->error&&begin&&!yyjson_is_null(begin)){if(!timestamp(yyjson_get_str(begin),&start))s->error="INVALID_TIME";else if(nowerror="LICENSE_NOT_STARTED";} if(!s->error&&end&&!yyjson_is_null(end)){if(!timestamp(yyjson_get_str(end),&expiry))s->error="INVALID_TIME";else if(now>=expiry)s->error="LICENSE_EXPIRED";} if(!s->error&&strcmp(source,"online")&&now>grace)s->error="OFFLINE_GRACE_EXPIRED"; if(!s->error){yyjson_val *features=yyjson_obj_get(p,"features");if(!yyjson_is_obj(features))s->error="INVALID_RESPONSE";else{*enabled=yyjson_is_true(yyjson_obj_get(features,"pdf_export"));yyjson_val *limit=yyjson_obj_get(features,"seat_count");if(yyjson_is_raw(limit)){const char *raw=yyjson_get_raw(limit);char *tail;errno=0;long long value=strtoll(raw,&tail,10);if(!errno&&tail==raw+yyjson_get_len(limit)&&value>=0&&value<=9007199254740991LL)*seats=value;}}} yyjson_doc_free(doc);return s->error==NULL; } static void output(Session *s,int valid,const char *source,int signed_valid,int enabled,long long seats,const char *message) { yyjson_mut_doc *doc=yyjson_mut_doc_new(NULL);yyjson_mut_val *root=yyjson_mut_obj(doc);yyjson_mut_doc_set_root(doc,root);yyjson_mut_obj_add_bool(doc,root,"valid",valid!=0);yyjson_mut_obj_add_str(doc,root,"source",source);yyjson_mut_obj_add_bool(doc,root,"signature_valid",signed_valid!=0);yyjson_mut_obj_add_bool(doc,root,"pdf_export_allowed",enabled!=0);if(seats>=0)yyjson_mut_obj_add_sint(doc,root,"seat_count",seats);else yyjson_mut_obj_add_null(doc,root,"seat_count"); if(s->error)yyjson_mut_obj_add_str(doc,root,"error_code",s->error);else yyjson_mut_obj_add_null(doc,root,"error_code");if(*s->request_id)yyjson_mut_obj_add_str(doc,root,"request_id",s->request_id);else yyjson_mut_obj_add_null(doc,root,"request_id");yyjson_mut_obj_add_str(doc,root,"message",message);char *raw=yyjson_mut_write(doc,0,NULL);if(raw)puts(raw);free(raw);yyjson_mut_doc_free(doc); } static int setup(Session *s) { const char *app=getenv("KDP_APP_ID"),*base=getenv("KDP_BASE_URL"),*root=getenv("KDP_STATE_DIR"),*key=getenv("KDP_TRUSTED_KEY_FILE");if(!identifier(app,160)||!root||*root!='/'||!key||*key!='/')return 0;s->app=strdup(app);s->base=strdup(base?base:"https://licensing.kdpika.com/api/v1");if(!s->app||!s->base)return 0; CURLU *url=curl_url();if(!url)return 0;CURLUcode code=curl_url_set(url,CURLUPART_URL,s->base,0);char *scheme=NULL,*host=NULL,*path=NULL,*value=NULL;int ok=code==CURLUE_OK&&curl_url_get(url,CURLUPART_SCHEME,&scheme,0)==CURLUE_OK&&curl_url_get(url,CURLUPART_HOST,&host,0)==CURLUE_OK&&curl_url_get(url,CURLUPART_PATH,&path,0)==CURLUE_OK; int local=ok&&!strcmp(scheme,"http")&&(!strcmp(host,"127.0.0.1")||!strcmp(host,"localhost"))&&getenv("KDP_ALLOW_LOOPBACK")&&!strcmp(getenv("KDP_ALLOW_LOOPBACK"),"1");if(!ok||(strcmp(scheme,"https")&&!local)||strcmp(path,"/api/v1"))ok=0; CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};for(size_t i=0;i<4;i++)if(curl_url_get(url,forbidden[i],&value,0)==CURLUE_OK){ok=0;curl_free(value);value=NULL;}curl_free(scheme);curl_free(host);curl_free(path);curl_url_cleanup(url);if(!ok)return 0; s->trusted=read_private(key,&s->trusted_size);yyjson_doc *trusted=s->trusted?parse(s->trusted,s->trusted_size):NULL;if(!trusted)return 0;ok=equals(yyjson_doc_get_root(trusted),"algorithm","Ed25519")&&equals(yyjson_doc_get_root(trusted),"encoding","base64url");yyjson_doc_free(trusted);if(!ok||!directory(root))return 0; char *scope=join(s->base,s->app);if(!scope)return 0;char digest[65];hash(scope,digest);free(scope);char segment[24];snprintf(segment,sizeof segment,"/%.20s",digest);s->directory=join(root,segment);if(!s->directory||!directory(s->directory))return 0; char *lock=join(s->directory,"/.lock");if(!lock)return 0;s->lock=open(lock,O_CREAT|O_RDWR|O_NOFOLLOW,0600);free(lock);if(s->lock<0||flock(s->lock,LOCK_EX|LOCK_NB))return 0; char *identity=join(s->directory,"/installation.json");if(!identity)return 0;size_t size=0;char *raw=read_private(identity,&size); if(!raw&&errno==ENOENT){unsigned char random[16];randombytes_buf(random,sizeof random);char hex[33];sodium_bin2hex(hex,sizeof hex,random,sizeof random);char body[100];snprintf(body,sizeof body,"{\"installation_id\":\"lmp-install-%.8s-%.4s-%.4s-%.4s-%.12s\"}",hex,hex+8,hex+12,hex+16,hex+20);if(!write_atomic(s,"/installation.json",body)){free(identity);return 0;}raw=read_private(identity,&size);}free(identity);yyjson_doc *doc=raw?parse(raw,size):NULL;free(raw);if(!doc)return 0;const char *id=string(yyjson_doc_get_root(doc),"installation_id");if(identifier(id,48)&&strlen(id)==48&&!strncmp(id,"lmp-install-",12))s->installation=strdup(id);yyjson_doc_free(doc);return s->installation!=NULL; } static int execute(Session *s,const char *action) { char *file=join(s->directory,"/session.json");size_t size=0;struct stat stat;int exists=file&&lstat(file,&stat)==0;char *raw=exists?read_private(file,&size):NULL;free(file);if(exists&&!raw)return 2;yyjson_doc *doc=raw?parse(raw,size):NULL;free(raw);if(exists&&!doc)return 2; yyjson_val *state=doc?yyjson_doc_get_root(doc):NULL;const char *token=state?string(state,"token"):NULL,*checkpoint=state?string(state,"checkpoint"):NULL,*cached=state?string(state,"envelope"):NULL;char *saved_token=token?strdup(token):NULL,*saved_time=checkpoint?strdup(checkpoint):NULL,*envelope=cached?strdup(cached):NULL;yyjson_doc_free(doc); int result=2,enabled=0;long long seats=-1;char *clock=NULL; if(exists&&(!saved_token||strncmp(saved_token,"act_",4)||!identifier(saved_token,256)||!saved_time))goto finish; if(!strcmp(action,"status")){char identity[65];hash(s->installation,identity);printf("{\"activated\":%s,\"installation_hash\":\"%s\",\"storage_security\":\"permission-protected-file\"}\n",exists?"true":"false",identity);result=0;goto finish;} if(!strcmp(action,"activate")){ const char *license=getenv("KDP_LICENSE_KEY"),*email=getenv("KDP_CUSTOMER_EMAIL");if(!safe_text(license,200)||strncmp(license,"TEST",4)||(email&&*email&&!safe_text(email,320)))goto finish;if(exists){s->error="ALREADY_ACTIVATED";goto refused;} yyjson_mut_doc *body=yyjson_mut_doc_new(NULL);yyjson_mut_val *value=yyjson_mut_obj(body);yyjson_mut_doc_set_root(body,value);yyjson_mut_obj_add_str(body,value,"license_key",license);yyjson_mut_obj_add_str(body,value,"device_id",s->installation);yyjson_mut_obj_add_str(body,value,"device_name","Native REST example");yyjson_mut_obj_add_str(body,value,"platform","posix");yyjson_mut_obj_add_str(body,value,"app_version","1.0.0");if(email&&*email)yyjson_mut_obj_add_str(body,value,"email",email);char *text=yyjson_mut_write(body,0,NULL);yyjson_doc *reply=text?request(s,"/licenses/activate",text,NULL):NULL;free(text);yyjson_mut_doc_free(body);if(!reply)goto refused; yyjson_val *response=yyjson_doc_get_root(reply);const char *new_token=string(response,"activation_token");if(!identifier(new_token,256)||strncmp(new_token,"act_",4)){s->error="INVALID_RESPONSE";yyjson_doc_free(reply);goto refused;}saved_token=strdup(new_token);envelope=dump(yyjson_obj_get(response,"license"));yyjson_doc_free(reply);if(!envelope){s->error="INVALID_RESPONSE";goto refused;} if(!verify(s,envelope,"online",NULL,&enabled,&seats)){const char *failure=s->error;yyjson_doc *released=request(s,"/licenses/deactivate","{}",saved_token);yyjson_doc_free(released);s->error=failure;goto refused;} clock=now_string();if(!clock||!save(s,saved_token,envelope,clock))goto finish;output(s,1,"online",1,enabled,seats,"Your license is ready.");result=0;goto finish; } if(!exists){s->error="NOT_ACTIVATED";goto refused;} if(!strcmp(action,"deactivate")){yyjson_doc *reply=request(s,"/licenses/deactivate","{}",saved_token);if(!reply)goto refused;int ok=yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(reply),"deactivated"));yyjson_doc_free(reply);if(!ok){s->error="INVALID_RESPONSE";goto refused;}clear(s);output(s,1,"online",0,0,-1,"This installation has been deactivated.");result=0;goto finish;} if(!strcmp(action,"check")&&envelope){if(verify(s,envelope,"cache",saved_time,&enabled,&seats)){yyjson_doc *env=parse(envelope,strlen(envelope));double next;int use=env&×tamp(string(yyjson_obj_get(yyjson_doc_get_root(env),"payload"),"next_validation_at"),&next)&&(double)time(NULL)error&&!strcmp(s->error,"NETWORK_ERROR")){if(!envelope){s->error="OFFLINE_CACHE_MISSING";goto refused;}source="offline";} else{if(s->error&&(!strcmp(s->error,"INVALID_TOKEN")||!strcmp(s->error,"DEVICE_MISMATCH")||!strcmp(s->error,"LICENSE_REVOKED")||!strcmp(s->error,"LICENSE_EXPIRED")||!strcmp(s->error,"LICENSE_CANCELLED")))clear(s);else if(!save(s,saved_token,NULL,saved_time))goto finish;goto refused;}} else{free(envelope);envelope=dump(yyjson_obj_get(yyjson_doc_get_root(reply),"license"));yyjson_doc_free(reply);if(!envelope){s->error="INVALID_RESPONSE";goto refused;}} if(!verify(s,envelope,source,saved_time,&enabled,&seats)){if(!strcmp(source,"online"))save(s,saved_token,NULL,saved_time);goto refused;}clock=now_string();if(!clock||!save(s,saved_token,envelope,clock))goto finish;output(s,1,source,1,enabled,seats,"Your license is ready.");result=0;goto finish;} refused:output(s,0,"local",0,0,-1,"Your license could not be confirmed. Connect and try again, or contact your software provider.");result=1; finish:free(saved_token);free(saved_time);free(envelope);free(clock);return result; } int kdpika_session_main(int argc,char **argv){Session s={0};s.lock=-1;int result=2;if(argc==2&&(!strcmp(argv[1],"status")||!strcmp(argv[1],"activate")||!strcmp(argv[1],"check")||!strcmp(argv[1],"validate")||!strcmp(argv[1],"deactivate"))&&sodium_init()>=0&&curl_global_init(CURL_GLOBAL_DEFAULT)==CURLE_OK&&setup(&s))result=execute(&s,argv[1]); if(result==2)fputs("Check application settings and private storage. Contact your software provider if the problem continues.\n",stderr);if(s.lock>=0)close(s.lock);free(s.app);free(s.base);free(s.directory);free(s.installation);free(s.trusted);curl_global_cleanup();return result;} ``` 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 POSIX C core using owner-only files, flock, atomic writes, libcurl TLS verification and exact yyjson numeric lexemes. The script defaults SODIUM_PREFIX to /usr/local; set your actual installed prefix. Its dependency downloads are pinned by SHA-256. macOS is tested; Linux requires compatible POSIX APIs/libcurl; native Windows needs a deliberate storage/time/locking port. 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) # C++ REST client Canonical: https://licensing.kdpika.com/docs/v1/sdks/cpp --- title: "C++ REST client" description: "Build a complete C++ Sandbox licensing client with verified signatures, persistent identity, private state, offline policy and safe feature gating." slug: "/docs/v1/sdks/cpp" section: "SDKs and languages" order: 60 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 C++ 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 C++17 entry point plus C11/POSIX core, libcurl, yyjson0.13.0, libsodium1.0.20. A native C++17 executable links the full C ABI session core. The core is compiled as C11, not compiled accidentally as C++; no Python/Node bridge or hidden licensing service is involved. C and C++ intentionally share state format/namespace. POSIX storage/clock APIs are not a claim of native Windows support. ## 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-cpp-rest-example-1.0.0.zip](/docs/downloads/kdpika-cpp-rest-example-1.0.0.zip) · 11215 bytes · version 1.0.0 SHA-256: 942f52d4601709843bdba099a9993569ba2851b193801f3c72165d3949aa108e 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/cpp. 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 SODIUM_PREFIX=/opt/homebrew/opt/libsodium 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 ```cpp // C++17 entry point. Link the complete C ABI native session core, yyjson, // libcurl and libsodium. No extra Python/Node process or SDK dependency. // Run ./session status|activate|check|validate|deactivate; safe JSON decisions. extern "C" int kdpika_session_main(int argc,char **argv); int main(int argc,char **argv){return kdpika_session_main(argc,argv);} ``` ```c /* Complete Sandbox REST session core; C11/POSIX, libcurl, yyjson0.13.0, * libsodium1.0.20. Linked by both C and C++ entry points. Never logs secrets. * Run status|activate|check|validate|deactivate with documented KDP_* config. */ #define _DARWIN_C_SOURCE #define _GNU_SOURCE #include "../../security/c/canonical.h" #include #include #include #include #include #include #include #include typedef struct { char *app,*base,*directory,*installation,*trusted;size_t trusted_size;int lock;const char *error;char request_id[128]; } Session; typedef struct { char *data;size_t size;int oversized; } Buffer; static int safe_text(const char *value,size_t limit) {if(!value||!*value||strlen(value)>limit)return 0;for(const char *p=value;*p;p++)if(*p=='\r'||*p=='\n')return 0;return 1;} static int identifier(const char *value,size_t limit) {if(!safe_text(value,limit))return 0;for(const char *p=value;*p;p++)if(!((*p>='a'&&*p<='z')||(*p>='A'&&*p<='Z')||(*p>='0'&&*p<='9')||*p=='_'||*p=='-'))return 0;return 1;} static const char *string(yyjson_val *value,const char *name){return yyjson_get_str(yyjson_obj_get(value,name));} static char *join(const char *a,const char *b){size_t size=strlen(a)+strlen(b)+1;char *v=(char*)malloc(size);if(v)snprintf(v,size,"%s%s",a,b);return v;} static void hash(const char *value,char out[65]) {unsigned char digest[32];crypto_hash_sha256(digest,(const unsigned char*)value,strlen(value));sodium_bin2hex(out,65,digest,32);} static char *read_private(const char *file,size_t *size) { struct stat stat;int fd=open(file,O_RDONLY|O_NOFOLLOW);if(fd<0)return NULL; if(fstat(fd,&stat)||!S_ISREG(stat.st_mode)||stat.st_size>1048576||stat.st_size<0){close(fd);return NULL;} char *raw=(char*)malloc((size_t)stat.st_size+1);if(!raw){close(fd);return NULL;}size_t n=0; while(n<(size_t)stat.st_size){ssize_t got=read(fd,raw+n,(size_t)stat.st_size-n);if(got<=0){free(raw);close(fd);return NULL;}n+=(size_t)got;}close(fd);raw[n]=0;*size=n;return raw; } static int write_atomic(Session *s,const char *name,const char *raw) { char *template=join(s->directory,"/write-XXXXXX");if(!template)return 0;int fd=mkstemp(template);if(fd<0){free(template);return 0;}int ok=fchmod(fd,0600)==0;size_t n=0,len=strlen(raw); while(ok&&ndirectory,name);if(ok)ok=target&&rename(template,target)==0;free(target);free(template);return ok; } static char *dump(yyjson_val *value){Output result={NULL,0,0,1};canonical(&result,value,0);if(!result.ok){free(result.data);return NULL;}return result.data;} static int directory(const char *path) {struct stat st;if(mkdir(path,0700)&&errno!=EEXIST)return 0;if(lstat(path,&st)||!S_ISDIR(st.st_mode)||S_ISLNK(st.st_mode))return 0;return chmod(path,0700)==0;} static char *now_string(void){time_t now=time(NULL);struct tm utc;if(!gmtime_r(&now,&utc))return NULL;char text[40];if(!strftime(text,sizeof text,"%Y-%m-%dT%H:%M:%SZ",&utc))return NULL;return strdup(text);} static int timestamp(const char *text,double *result) { if(!text)return 0;struct tm tm={0};char *tail=strptime(text,"%Y-%m-%dT%H:%M:%S",&tm);if(!tail)return 0;double fraction=0; if(*tail=='.'){char *end;fraction=strtod(tail,&end);if(end==tail)return 0;tail=end;} int offset=0;if(*tail=='Z')tail++;else if(*tail=='+'||*tail=='-'){int sign=*tail++=='+'?1:-1;int hours,minutes,used=0;if(sscanf(tail,"%2d:%2d%n",&hours,&minutes,&used)!=2||used!=5||hours>23||minutes>59)return 0;offset=sign*(hours*3600+minutes*60);tail+=used;}else return 0; if(*tail)return 0;*result=(double)timegm(&tm)+fraction-offset;return 1; } static size_t receive(char *data,size_t size,size_t count,void *context) { Buffer *b=(Buffer*)context;if(size&&count>SIZE_MAX/size)return 0;size_t n=size*count;if(n>1048576-b->size){b->oversized=1;return 0;}char *next=(char*)realloc(b->data,b->size+n+1);if(!next)return 0;b->data=next;memcpy(next+b->size,data,n);b->size+=n;next[b->size]=0;return n; } static yyjson_doc *request(Session *s,const char *endpoint,const char *body,const char *token) { s->error=NULL;s->request_id[0]=0;CURL *curl=curl_easy_init();if(!curl){s->error="NETWORK_ERROR";return NULL;}char *url=join(s->base,endpoint);Buffer buffer={NULL,0,0};struct curl_slist *headers=NULL; headers=curl_slist_append(headers,"Accept: application/json");headers=curl_slist_append(headers,"Content-Type: application/json");char *auth=join(token?"Authorization: Bearer ":"X-App-ID: ",token?token:s->app);headers=curl_slist_append(headers,auth);free(auth); if(token){char *installation=join("X-Installation-ID: ",s->installation);headers=curl_slist_append(headers,installation);free(installation);}else headers=curl_slist_append(headers,"X-Environment: SANDBOX"); curl_easy_setopt(curl,CURLOPT_URL,url);curl_easy_setopt(curl,CURLOPT_HTTPHEADER,headers);curl_easy_setopt(curl,CURLOPT_POSTFIELDS,body);curl_easy_setopt(curl,CURLOPT_POSTFIELDSIZE,(long)strlen(body));curl_easy_setopt(curl,CURLOPT_TIMEOUT,15L);curl_easy_setopt(curl,CURLOPT_CONNECTTIMEOUT,5L); curl_easy_setopt(curl,CURLOPT_FOLLOWLOCATION,0L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYPEER,1L);curl_easy_setopt(curl,CURLOPT_SSL_VERIFYHOST,2L);curl_easy_setopt(curl,CURLOPT_PROTOCOLS_STR,"http,https");curl_easy_setopt(curl,CURLOPT_USERAGENT,"KDPika-REST-native-example/1.0");curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,receive);curl_easy_setopt(curl,CURLOPT_WRITEDATA,&buffer); CURLcode code=curl_easy_perform(curl);long status=0;curl_easy_getinfo(curl,CURLINFO_RESPONSE_CODE,&status);curl_easy_cleanup(curl);curl_slist_free_all(headers);free(url); if(code!=CURLE_OK){s->error=buffer.oversized?"INVALID_RESPONSE":"NETWORK_ERROR";free(buffer.data);return NULL;} yyjson_doc *doc=buffer.data?parse(buffer.data,buffer.size):NULL;free(buffer.data);if(!doc){s->error="INVALID_RESPONSE";return NULL;}yyjson_val *root=yyjson_doc_get_root(doc);const char *id=string(root,"request_id");if(id&&strncmp(id,"req_",4)==0&&identifier(id,124))snprintf(s->request_id,sizeof s->request_id,"%s",id); if(status<200||status>=300||!yyjson_is_true(yyjson_obj_get(root,"success"))){const char *error=string(yyjson_obj_get(root,"error"),"code");s->error="HTTP_ERROR";if(error&&identifier(error,64)){static _Thread_local char copied[65];snprintf(copied,sizeof copied,"%s",error);s->error=copied;}yyjson_doc_free(doc);return NULL;}return doc; } static int save(Session *s,const char *token,const char *envelope,const char *checkpoint) { yyjson_mut_doc *doc=yyjson_mut_doc_new(NULL);yyjson_mut_val *root=yyjson_mut_obj(doc);yyjson_mut_doc_set_root(doc,root); yyjson_mut_obj_add_str(doc,root,"token",token);if(envelope)yyjson_mut_obj_add_str(doc,root,"envelope",envelope);else yyjson_mut_obj_add_null(doc,root,"envelope");yyjson_mut_obj_add_str(doc,root,"checkpoint",checkpoint); char *raw=yyjson_mut_write(doc,0,NULL);int ok=raw&&write_atomic(s,"/session.json",raw);free(raw);yyjson_mut_doc_free(doc);return ok; } static void clear(Session *s) {char *file=join(s->directory,"/session.json");if(file){unlink(file);free(file);}} static int verify(Session *s,const char *envelope,const char *source,const char *checkpoint,int *enabled,long long *seats) { s->error=NULL;*enabled=0;*seats=-1;if(!verify_envelope(s->trusted,s->trusted_size,envelope,strlen(envelope))){s->error="SIGNATURE_INVALID";return 0;} yyjson_doc *doc=parse(envelope,strlen(envelope));if(!doc){s->error="INVALID_RESPONSE";return 0;}yyjson_val *p=yyjson_obj_get(yyjson_doc_get_root(doc),"payload");char identity[65];hash(s->installation,identity); if(!equals(p,"product_id",s->app)||!equals(p,"environment","sandbox")||!equals(yyjson_obj_get(p,"device"),"installation_hash",identity))s->error="BINDING_MISMATCH"; double server=0,next=0,grace=0,latest=0,expiry=0,start=0,now=(double)time(NULL); if(!s->error&&(!timestamp(string(p,"server_time"),&server)||!timestamp(string(p,"next_validation_at"),&next)||!timestamp(string(p,"offline_grace_until"),&grace)||nexterror="INVALID_TIME"; latest=server;if(!s->error&&checkpoint){double previous;if(!timestamp(checkpoint,&previous))s->error="INVALID_TIME";else if(previous>latest)latest=previous;} if(!s->error&&now+300error="CLOCK_ROLLBACK_DETECTED";if(!s->error&&!equals(p,"status","active"))s->error="LICENSE_NOT_ACTIVE"; yyjson_val *begin=yyjson_obj_get(p,"starts_at"),*end=yyjson_obj_get(p,"expires_at"); if(!s->error&&begin&&!yyjson_is_null(begin)){if(!timestamp(yyjson_get_str(begin),&start))s->error="INVALID_TIME";else if(nowerror="LICENSE_NOT_STARTED";} if(!s->error&&end&&!yyjson_is_null(end)){if(!timestamp(yyjson_get_str(end),&expiry))s->error="INVALID_TIME";else if(now>=expiry)s->error="LICENSE_EXPIRED";} if(!s->error&&strcmp(source,"online")&&now>grace)s->error="OFFLINE_GRACE_EXPIRED"; if(!s->error){yyjson_val *features=yyjson_obj_get(p,"features");if(!yyjson_is_obj(features))s->error="INVALID_RESPONSE";else{*enabled=yyjson_is_true(yyjson_obj_get(features,"pdf_export"));yyjson_val *limit=yyjson_obj_get(features,"seat_count");if(yyjson_is_raw(limit)){const char *raw=yyjson_get_raw(limit);char *tail;errno=0;long long value=strtoll(raw,&tail,10);if(!errno&&tail==raw+yyjson_get_len(limit)&&value>=0&&value<=9007199254740991LL)*seats=value;}}} yyjson_doc_free(doc);return s->error==NULL; } static void output(Session *s,int valid,const char *source,int signed_valid,int enabled,long long seats,const char *message) { yyjson_mut_doc *doc=yyjson_mut_doc_new(NULL);yyjson_mut_val *root=yyjson_mut_obj(doc);yyjson_mut_doc_set_root(doc,root);yyjson_mut_obj_add_bool(doc,root,"valid",valid!=0);yyjson_mut_obj_add_str(doc,root,"source",source);yyjson_mut_obj_add_bool(doc,root,"signature_valid",signed_valid!=0);yyjson_mut_obj_add_bool(doc,root,"pdf_export_allowed",enabled!=0);if(seats>=0)yyjson_mut_obj_add_sint(doc,root,"seat_count",seats);else yyjson_mut_obj_add_null(doc,root,"seat_count"); if(s->error)yyjson_mut_obj_add_str(doc,root,"error_code",s->error);else yyjson_mut_obj_add_null(doc,root,"error_code");if(*s->request_id)yyjson_mut_obj_add_str(doc,root,"request_id",s->request_id);else yyjson_mut_obj_add_null(doc,root,"request_id");yyjson_mut_obj_add_str(doc,root,"message",message);char *raw=yyjson_mut_write(doc,0,NULL);if(raw)puts(raw);free(raw);yyjson_mut_doc_free(doc); } static int setup(Session *s) { const char *app=getenv("KDP_APP_ID"),*base=getenv("KDP_BASE_URL"),*root=getenv("KDP_STATE_DIR"),*key=getenv("KDP_TRUSTED_KEY_FILE");if(!identifier(app,160)||!root||*root!='/'||!key||*key!='/')return 0;s->app=strdup(app);s->base=strdup(base?base:"https://licensing.kdpika.com/api/v1");if(!s->app||!s->base)return 0; CURLU *url=curl_url();if(!url)return 0;CURLUcode code=curl_url_set(url,CURLUPART_URL,s->base,0);char *scheme=NULL,*host=NULL,*path=NULL,*value=NULL;int ok=code==CURLUE_OK&&curl_url_get(url,CURLUPART_SCHEME,&scheme,0)==CURLUE_OK&&curl_url_get(url,CURLUPART_HOST,&host,0)==CURLUE_OK&&curl_url_get(url,CURLUPART_PATH,&path,0)==CURLUE_OK; int local=ok&&!strcmp(scheme,"http")&&(!strcmp(host,"127.0.0.1")||!strcmp(host,"localhost"))&&getenv("KDP_ALLOW_LOOPBACK")&&!strcmp(getenv("KDP_ALLOW_LOOPBACK"),"1");if(!ok||(strcmp(scheme,"https")&&!local)||strcmp(path,"/api/v1"))ok=0; CURLUPart forbidden[]={CURLUPART_USER,CURLUPART_PASSWORD,CURLUPART_QUERY,CURLUPART_FRAGMENT};for(size_t i=0;i<4;i++)if(curl_url_get(url,forbidden[i],&value,0)==CURLUE_OK){ok=0;curl_free(value);value=NULL;}curl_free(scheme);curl_free(host);curl_free(path);curl_url_cleanup(url);if(!ok)return 0; s->trusted=read_private(key,&s->trusted_size);yyjson_doc *trusted=s->trusted?parse(s->trusted,s->trusted_size):NULL;if(!trusted)return 0;ok=equals(yyjson_doc_get_root(trusted),"algorithm","Ed25519")&&equals(yyjson_doc_get_root(trusted),"encoding","base64url");yyjson_doc_free(trusted);if(!ok||!directory(root))return 0; char *scope=join(s->base,s->app);if(!scope)return 0;char digest[65];hash(scope,digest);free(scope);char segment[24];snprintf(segment,sizeof segment,"/%.20s",digest);s->directory=join(root,segment);if(!s->directory||!directory(s->directory))return 0; char *lock=join(s->directory,"/.lock");if(!lock)return 0;s->lock=open(lock,O_CREAT|O_RDWR|O_NOFOLLOW,0600);free(lock);if(s->lock<0||flock(s->lock,LOCK_EX|LOCK_NB))return 0; char *identity=join(s->directory,"/installation.json");if(!identity)return 0;size_t size=0;char *raw=read_private(identity,&size); if(!raw&&errno==ENOENT){unsigned char random[16];randombytes_buf(random,sizeof random);char hex[33];sodium_bin2hex(hex,sizeof hex,random,sizeof random);char body[100];snprintf(body,sizeof body,"{\"installation_id\":\"lmp-install-%.8s-%.4s-%.4s-%.4s-%.12s\"}",hex,hex+8,hex+12,hex+16,hex+20);if(!write_atomic(s,"/installation.json",body)){free(identity);return 0;}raw=read_private(identity,&size);}free(identity);yyjson_doc *doc=raw?parse(raw,size):NULL;free(raw);if(!doc)return 0;const char *id=string(yyjson_doc_get_root(doc),"installation_id");if(identifier(id,48)&&strlen(id)==48&&!strncmp(id,"lmp-install-",12))s->installation=strdup(id);yyjson_doc_free(doc);return s->installation!=NULL; } static int execute(Session *s,const char *action) { char *file=join(s->directory,"/session.json");size_t size=0;struct stat stat;int exists=file&&lstat(file,&stat)==0;char *raw=exists?read_private(file,&size):NULL;free(file);if(exists&&!raw)return 2;yyjson_doc *doc=raw?parse(raw,size):NULL;free(raw);if(exists&&!doc)return 2; yyjson_val *state=doc?yyjson_doc_get_root(doc):NULL;const char *token=state?string(state,"token"):NULL,*checkpoint=state?string(state,"checkpoint"):NULL,*cached=state?string(state,"envelope"):NULL;char *saved_token=token?strdup(token):NULL,*saved_time=checkpoint?strdup(checkpoint):NULL,*envelope=cached?strdup(cached):NULL;yyjson_doc_free(doc); int result=2,enabled=0;long long seats=-1;char *clock=NULL; if(exists&&(!saved_token||strncmp(saved_token,"act_",4)||!identifier(saved_token,256)||!saved_time))goto finish; if(!strcmp(action,"status")){char identity[65];hash(s->installation,identity);printf("{\"activated\":%s,\"installation_hash\":\"%s\",\"storage_security\":\"permission-protected-file\"}\n",exists?"true":"false",identity);result=0;goto finish;} if(!strcmp(action,"activate")){ const char *license=getenv("KDP_LICENSE_KEY"),*email=getenv("KDP_CUSTOMER_EMAIL");if(!safe_text(license,200)||strncmp(license,"TEST",4)||(email&&*email&&!safe_text(email,320)))goto finish;if(exists){s->error="ALREADY_ACTIVATED";goto refused;} yyjson_mut_doc *body=yyjson_mut_doc_new(NULL);yyjson_mut_val *value=yyjson_mut_obj(body);yyjson_mut_doc_set_root(body,value);yyjson_mut_obj_add_str(body,value,"license_key",license);yyjson_mut_obj_add_str(body,value,"device_id",s->installation);yyjson_mut_obj_add_str(body,value,"device_name","Native REST example");yyjson_mut_obj_add_str(body,value,"platform","posix");yyjson_mut_obj_add_str(body,value,"app_version","1.0.0");if(email&&*email)yyjson_mut_obj_add_str(body,value,"email",email);char *text=yyjson_mut_write(body,0,NULL);yyjson_doc *reply=text?request(s,"/licenses/activate",text,NULL):NULL;free(text);yyjson_mut_doc_free(body);if(!reply)goto refused; yyjson_val *response=yyjson_doc_get_root(reply);const char *new_token=string(response,"activation_token");if(!identifier(new_token,256)||strncmp(new_token,"act_",4)){s->error="INVALID_RESPONSE";yyjson_doc_free(reply);goto refused;}saved_token=strdup(new_token);envelope=dump(yyjson_obj_get(response,"license"));yyjson_doc_free(reply);if(!envelope){s->error="INVALID_RESPONSE";goto refused;} if(!verify(s,envelope,"online",NULL,&enabled,&seats)){const char *failure=s->error;yyjson_doc *released=request(s,"/licenses/deactivate","{}",saved_token);yyjson_doc_free(released);s->error=failure;goto refused;} clock=now_string();if(!clock||!save(s,saved_token,envelope,clock))goto finish;output(s,1,"online",1,enabled,seats,"Your license is ready.");result=0;goto finish; } if(!exists){s->error="NOT_ACTIVATED";goto refused;} if(!strcmp(action,"deactivate")){yyjson_doc *reply=request(s,"/licenses/deactivate","{}",saved_token);if(!reply)goto refused;int ok=yyjson_is_true(yyjson_obj_get(yyjson_doc_get_root(reply),"deactivated"));yyjson_doc_free(reply);if(!ok){s->error="INVALID_RESPONSE";goto refused;}clear(s);output(s,1,"online",0,0,-1,"This installation has been deactivated.");result=0;goto finish;} if(!strcmp(action,"check")&&envelope){if(verify(s,envelope,"cache",saved_time,&enabled,&seats)){yyjson_doc *env=parse(envelope,strlen(envelope));double next;int use=env&×tamp(string(yyjson_obj_get(yyjson_doc_get_root(env),"payload"),"next_validation_at"),&next)&&(double)time(NULL)error&&!strcmp(s->error,"NETWORK_ERROR")){if(!envelope){s->error="OFFLINE_CACHE_MISSING";goto refused;}source="offline";} else{if(s->error&&(!strcmp(s->error,"INVALID_TOKEN")||!strcmp(s->error,"DEVICE_MISMATCH")||!strcmp(s->error,"LICENSE_REVOKED")||!strcmp(s->error,"LICENSE_EXPIRED")||!strcmp(s->error,"LICENSE_CANCELLED")))clear(s);else if(!save(s,saved_token,NULL,saved_time))goto finish;goto refused;}} else{free(envelope);envelope=dump(yyjson_obj_get(yyjson_doc_get_root(reply),"license"));yyjson_doc_free(reply);if(!envelope){s->error="INVALID_RESPONSE";goto refused;}} if(!verify(s,envelope,source,saved_time,&enabled,&seats)){if(!strcmp(source,"online"))save(s,saved_token,NULL,saved_time);goto refused;}clock=now_string();if(!clock||!save(s,saved_token,envelope,clock))goto finish;output(s,1,source,1,enabled,seats,"Your license is ready.");result=0;goto finish;} refused:output(s,0,"local",0,0,-1,"Your license could not be confirmed. Connect and try again, or contact your software provider.");result=1; finish:free(saved_token);free(saved_time);free(envelope);free(clock);return result; } int kdpika_session_main(int argc,char **argv){Session s={0};s.lock=-1;int result=2;if(argc==2&&(!strcmp(argv[1],"status")||!strcmp(argv[1],"activate")||!strcmp(argv[1],"check")||!strcmp(argv[1],"validate")||!strcmp(argv[1],"deactivate"))&&sodium_init()>=0&&curl_global_init(CURL_GLOBAL_DEFAULT)==CURLE_OK&&setup(&s))result=execute(&s,argv[1]); if(result==2)fputs("Check application settings and private storage. Contact your software provider if the problem continues.\n",stderr);if(s.lock>=0)close(s.lock);free(s.app);free(s.base);free(s.directory);free(s.installation);free(s.trusted);curl_global_cleanup();return result;} ``` 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 A native C++17 executable links the full C ABI session core. The core is compiled as C11, not compiled accidentally as C++; no Python/Node bridge or hidden licensing service is involved. C and C++ intentionally share state format/namespace. POSIX storage/clock APIs are not a claim of native Windows support. 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) # Build your own licensing client Canonical: https://licensing.kdpika.com/docs/v1/sdks/build-your-own-client --- title: "Build your own licensing client" description: "Implement the real API v1 contract with a complete conformance checklist, signature vectors and actual Sandbox self-tests." slug: "/docs/v1/sdks/build-your-own-client" section: "SDKs and languages" order: 61 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/sdks/overview", "/docs/v1/security/test-vectors"] draft: false --- Implement the existing contract with independent key trust, stable installation state and fail-closed policy. ## What you will learn - Choose the client boundary and enforce every authorization layer. - Run all published signature vectors and a real [Sandbox](/docs/v1/start/glossary#sandbox) lifecycle. - Test storage, clock, offline and error behavior without fabricated success. ## Prerequisites - Complete [the Sandbox quickstart](/docs/v1/start/quickstart). - Read [signed responses](/docs/v1/security/signed-responses), [authentication](/docs/v1/start/authentication) and [offline mode](/docs/v1/security/offline-mode). ## Decide whether you need a custom client Use an [official SDK](/docs/v1/start/glossary#official-sdk) when its runtime and documented release limitations fit. Use a REST example when you need another native language, stricter application-specific policy or controlled storage. A custom client implements the existing API; it must not invent new server fields, endpoints, signing formats or offline extensions. The ten complete REST projects are working examples, not ten additional official SDK [products](/docs/v1/start/glossary#product). TypeScript uses lossless verification modules, Kotlin shares the Java core through JVM interop, and C++ links the complete C ABI core. These are explicit native-library boundaries, not hidden Python/Node subprocess dependencies. ## Conformance checklist | Area | Required behavior | Negative test | | --- | --- | --- | | Credentials | Public X-App-ID at [activation](/docs/v1/start/glossary#activation); Bearer act_ for the installation; lmp_ only on a [developer](/docs/v1/start/glossary#developer) backend | A [Live](/docs/v1/start/glossary#live) key or wrong environment never creates a test activation | | Installation | Generate random stable identity once, persist privately across processes/updates | Same namespace retains the exact installation hash; no hardware serial fallback | | HTTP | HTTPS peer/hostname verification, timeout, bounded JSON, no credential-bearing redirects | Nonlocal HTTP, URL credentials/query/fragment, redirect and oversized/invalid success are denied | | Parser | Preserve numeric lexemes; reject duplicate keys, malformed UTF-8/Unicode, trailing data and excessive depth | Integral floats, Unicode, duplicate-property and invalid-encoding vectors | | Key trust | Independently trust the raw Ed25519 public key and its derived key_id | Wrong key, algorithm, key ID, signature size or noncanonical base64url is denied | | Signature | Verify the exact canonical payload before policy or [features](/docs/v1/start/glossary#feature) | Modified payload/cache never unlocks a feature | | Binding | Match product, environment and SHA-256 installation binding | Correct signature for another product/environment/installation is still denied | | Status/time | Require active status; validate start/expiry and signed validation/grace dates | Suspended/revoked/expired/future-start policy refuses access | | Offline | Only transport failure can use still-eligible signed cache | Explicit HTTP refusal or invalid success cannot become offline permission | | Rollback | Compare trusted server/checkpoint time with the documented tolerance | A future checkpoint behind which the [device](/docs/v1/start/glossary#device) moved is rejected | | Features | Require accepted validity/signature before boolean/numeric access | Invalid results with true features, negative/fractional/noninteger limits are denied | | State | Atomic private writes, clear denied cache, serialize lifecycle operations | Corrupt/symlink state fails safely; concurrent writers cannot replace identity | | Mutations | Reconcile uncertain activation/release rather than blind retries | Lost response does not imply the server never consumed an activation | | UX/logging | Safe user text, bounded correlation ID, no key/token/[customer](/docs/v1/start/glossary#customer) payload output | Raw transport/exception/secret data never reaches a renderer or public log | The strict REST example policy is deliberately distinct from existing SDK implementation limitations. Do not state that all official SDKs enforce every row identically; compare the compatibility page and current source. Stronger client-side start/date checks do not change the server contract. ## Signature self-test procedure 1. Download the [complete verification archive](/docs/downloads/security-verifiers.zip). Expected: all source verifiers, public vectors and selftest.py; no private signing keys. 2. Install the exact dependencies described by your language's verification page. Expected: a native verifier that prints signature_valid=true/false with exit0/1. 3. Run selftest.py with the verifier command from that language page. Expected: PASS23, including valid signatures whose [license](/docs/v1/start/glossary#license) policy is expired. 4. Confirm that your application does not interpret signature_valid=true as current permission. Expected: the expired signature vector authenticates but fails expiry policy. 5. Keep vector sources and dependency locks pinned with your application tests. Expected: a byte/signature regression fails before distribution. The existing complete self-test harness is the one executed by this documentation program: ```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))) ``` Public vectors authenticate canonicalization/signatures only. Unicode/number conformance-only payloads are not complete licenses. Do not add fake product/time fields to them or turn them into fabricated server success. The separate actual Sandbox cycle establishes real binding and feature policy. ## Real Sandbox self-test 1. Create a fresh Sandbox product/plan/license with the actual documented dashboard/API workflow. Expected: a genuine App ID, TEST license and matching customer email; no developer key in the installed app. 2. Start with an empty private application namespace and activate with the wrong email. Expected: INVALID_LICENSE and no feature permission. 3. Activate with the correct data. Expected: genuine signed response, exact binding and explicit feature values. 4. Restart using the same state root and check. Expected: stable identity and eligible verified cache, not a new server activation. 5. Force online validation. Expected: an actual server decision; do not bypass signature failures. 6. Inject transport failure only in an isolated local test. Expected: bounded signed offline acceptance or a clear refusal; initial activation still requires network. 7. Inject an explicit HTTP refusal. Expected: no offline fallback grant and no stale authorization cache. 8. Modify a cached payload without resigning it. Expected: signature failure and no feature access. 9. Move the test checkpoint into the future. Expected: rollback rejection without editing a real signed timestamp. 10. Check the exact signed grace/expiry boundary with a test-only clock. Expected: the selected client's documented edge semantics; no clock override in the distributed production app. 11. Deactivate and restart. Expected: no credential/cache access and the same installation identity. Cumulative activation_count is not reset. The documentation runs actual Sandbox lifecycles in all ten languages, plus230 published signature-vector checks. TypeScript also tests exact signed grace/expiry instants using a test-only preload. These results do not certify every concurrent host, native OS vault, target binary or malformed-date combination. ## Build, storage and release checklist Keep trusted-key provisioning separate from license input. Do not automatically replace a pinned key because the envelope asks for another key_id. The current platform does not provide an historical key ring or an advertised overlap/rotation window; plan a controlled application update. Store the token and signed state outside executable extraction directories and public web roots. Use an OS vault where your application supports one, and document fallbacks honestly. A filename protected by Unix modes is not encrypted credential storage. Multiple tenants must not share one installation namespace. Serialize activation/checkpoint/deactivation writes, preserve random identity across upgrades and reconcile uncertain mutations. Deactivation releases a seat but does not reset cumulative [activation counts](/docs/v1/start/glossary#activation-count). Removing a package does not release the server seat or erase its application state. Before shipping, run real responses and cache reload through the bundled executable, test explicit refusal and offline recovery, check logs for secrets, and show users a simple support/retry path. Record platform/runtime/dependency versions and known limitations rather than silently patching or suppressing signature checks. ## Expected result Your integration authenticates signed state and handles failures without granting features. Installation identity, source version and test boundaries remain explicit. ## Common mistakes - Treating signature authenticity as complete license validity. - Retrying uncertain activation blindly or deleting identity to avoid a refusal. - Replacing a pinned trusted key from untrusted envelope data. ## Security notes Keep developer keys on trusted backends, pin independent key trust, preserve HTTPS/signature verification and never equate token presence with permission. Local machine owners can patch software; a successful build or checksum is not an anti-tamper certification. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Vector signature differs | Canonical bytes or key metadata differs | Compare the exact raw JSON and independently trusted key | | Offline unexpectedly allowed | HTTP refusal was mistaken for network loss | Keep transport/HTTP/signature/policy failure boundaries separate | | Repeated activations after upgrade | Namespace or identity storage changed | Restore the intended stable app state and reconcile extra seats | ## Next steps Review [the SDK downloads](/docs/v1/sdks/overview), [test vectors](/docs/v1/security/test-vectors) and [API errors](/docs/v1/api/errors) for the exact selected client. ## Related - [Installation identity](/docs/v1/security/installation-id) - [Secure token storage](/docs/v1/security/secure-token-storage) - [API conventions](/docs/v1/api/conventions) # Complete licensing sample applications Canonical: https://licensing.kdpika.com/docs/v1/sdks/sample-apps --- title: "Complete licensing sample applications" description: "Download six complete source applications with real Sandbox lifecycle, feature gates, offline/refusal handling and explicit platform test boundaries." slug: "/docs/v1/sdks/sample-apps" section: "SDKs and languages" order: 62 audience: "all" level: "advanced" last_updated: "2026-10-21" version: "v1" related: ["/docs/v1/sdks/overview", "/docs/v1/security/test-vectors"] draft: false --- Start from a complete application project, not a fragment that pretends to verify a [license](/docs/v1/start/glossary#license). ## What you will learn - Run six complete desktop/backend/CLI sample projects. - Exercise [activation](/docs/v1/start/glossary#activation), [features](/docs/v1/start/glossary#feature), offline/refusal and deactivation. - Understand exact storage, runtime and native-test boundaries. ## Prerequisites - Complete [the Sandbox quickstart](/docs/v1/start/quickstart). - Read [signed responses](/docs/v1/security/signed-responses), [authentication](/docs/v1/start/authentication) and [offline mode](/docs/v1/security/offline-mode). ## Current Python release For Python SDK 1.1.5, install [the PyPI package](https://pypi.org/project/kdpika-licensing/) and use the [versioned GitHub examples](https://github.com/kdpika/kdpika-licensing/tree/v1.1.5/examples). Keep the adjacent _common.py helper. The Python CLI 1.0.0 ZIP below is a retained legacy project bundled with SDK 1.0.0; it is not the current PyPI release and its old dependency instructions are not recommended for a new project. ## Six complete runnable projects | Application | Complete source ZIP | Bytes | SHA-256 | | --- | --- | --- | --- | | python-cli (legacy SDK 1.0.0) | [kdpika-python-cli-example-1.0.0.zip](/docs/downloads/kdpika-python-cli-example-1.0.0.zip) | 30069 | 3e4aac6849ec78e3fb75d715a00e4456145e6f36e5f67627814ec0caa0058d64 | | electron | [kdpika-electron-example-1.0.0.zip](/docs/downloads/kdpika-electron-example-1.0.0.zip) | 54905 | 47801b88c4b7c606fd9608ced391230daf967c8d91d9b9a05f8e0e699fd62dcc | | php-backend | [kdpika-php-backend-example-1.0.0.zip](/docs/downloads/kdpika-php-backend-example-1.0.0.zip) | 22371 | bdb3a8be5993b4f9f4657019ee9259068dce2c5df43e4038b356eb61082add12 | | csharp-desktop | [kdpika-csharp-desktop-example-1.0.0.zip](/docs/downloads/kdpika-csharp-desktop-example-1.0.0.zip) | 23340 | e7c33a00760ae099714d235f4ba560542aa68ad8c36aa04306619ecd9531aab5 | | go-cli | [kdpika-go-cli-example-1.0.0.zip](/docs/downloads/kdpika-go-cli-example-1.0.0.zip) | 10171 | 80db0d99c204b18879d730512073c3f4616c713d83184b8a0b9fa4f2add2b058 | | java-console | [kdpika-java-console-example-1.0.0.zip](/docs/downloads/kdpika-java-console-example-1.0.0.zip) | 12140 | 7bbcba603b2171487643bcafa5842b94d6816febcd6747bfeeadfd951b59c1e8 | Every ZIP contains its README, complete application code and exact included SDK/client source. No populated state directory, [customer](/docs/v1/start/glossary#customer) key, [activation token](/docs/v1/start/glossary#activation-token), private signing key or compiled unsigned binary is distributed. Package version1.0.0 here is a sample revision, not an additional [official SDK](/docs/v1/start/glossary#official-sdk) release. ## Choose an application | Project | Entry and purpose | Actual verification boundary | | --- | --- | --- | | Python CLI | main.py + complete session.py; official Python SDK | Real source CLI and frozen PyInstaller macOS executable; native Windows/Linux freeze not claimed | | Electron desktop | main-process service, isolated preload and renderer | Real macOS source window and unsigned packaged window; no code-signing/notarization or encrypted-vault claim | | PHP backend | Local authenticated session, CSRF and server-side feature action | Real loopback PHP/browser workflow including transport/refusal; intentionally not a public identity/multi-tenant system | | C# desktop | Full Windows WPF project plus shared LicenseController | Windows-targeted cross-build on macOS and real Sandbox controller tests; native WPF UI requires Windows | | Go CLI | Runnable shell entry to included native Go client | Fresh archive build and real lifecycle/offline/refusal checks | | Java console | Runnable shell entry to included Java session core | Fresh archive build, pinned jars and real lifecycle/offline/refusal checks | These are application examples, not a promised production [product](/docs/v1/start/glossary#product) for every UI framework. The official SDK runtime behavior—including current signature/cache defects—remains unchanged. C# real Unicode cache rejection is an honest refusal/online recovery path, not a fabricated offline success. ## Install the selected project 1. Download the complete project ZIP and verify its SHA-256. Expected: the table digest exactly matches. 2. Extract with the original layout. Expected: its README and all referenced SDK/client/verification sources are present. 3. Install the exact dependencies from that README. Expected: local source install/build, not an invented registry SDK. Development compilers stay on your machine, not on KDPika shared hosting. 4. Configure the [Public App ID](/docs/v1/start/glossary#public-app-id) and absolute private state path. Expected: a Sandbox namespace isolated from other apps/buyers. 5. Configure the independently trusted public key for Go/Java REST apps. Expected: controlled key trust before activation, not a key copied from an untrusted envelope. 6. Start the application with synthetic Sandbox data. Expected: no feature access until a current valid signed result grants that feature. ## Source and execution instructions ```text # Python CLI Sandbox application Requirements: Python3.9+, exact bundled SDK1.0.0; cryptography46.0.3. The archive contains the exact tested session.py beside this entry point, not a substitute SDK. Use .venv\Scripts\python.exe on Windows. The example deliberately selects file storage for reproducible tests, not a universal vault recommendation. clear-local never releases a server seat. ## Install and run ```sh python3 -m venv .venv .venv/bin/python -m pip install -r requirements.txt .venv/bin/python -m pip install --no-build-isolation ./kdpika-python-sdk .venv/bin/python main.py status ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ```text # Electron desktop Sandbox application Requirements: Electron44.9.0, electron-builder26.15.3, official JavaScript SDK1.0.0. The complete exact SDK directory is included for the local file dependency. Electron44 resolves its binary lazily at first start. Only main-process service owns licensing; contextIsolation/sandbox are enabled and renderer Node integration is disabled. Safe decisions cross narrow checked IPC. File storage is not automatically safeStorage encryption. The unsigned macOS directory build is a test artifact, not notarized distribution or Windows/Linux execution proof. ## Install and run ```sh npm ci npm start npm run package ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ```text # PHP backend Sandbox application Requirements: PHP8.2+, cURL, Sodium capability and Composer-installed official SDK1.0.0. This example rejects nonloopback callers. Set KDP_SDK_DIR to the absolute included SDK directory, KDP_STATE_DIR outside the served project and a random KDP_DEMO_ACCESS_CODE of at least24 bytes. Open the loopback URL, authenticate with the local access code and then activate/check. It uses real PHP sessions, CSRF tokens, safe output and server-side feature rechecks. It is not a production identity provider, public multi-tenant app or an instruction to deploy a local testing router. ## Install and run ```sh composer install --working-dir ./kdpika-php-sdk --no-plugins --no-scripts php -S 127.0.0.1:8200 -t . ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ```text # C# Windows WPF desktop Sandbox application Requirements: Windows desktop UI; net7.0-windows project plus official net7 SDK1.0.0/Bouncy Castle2.7.0. The exact SDK source directory is included for ProjectReference. Cross-build uses EnableWindowsTargeting on macOS; real WPF UI execution requires Windows and is not claimed on macOS. LicenseController.cs is shared verbatim between the UI and actual Sandbox controller tests. SDK Unicode cache rejection can prevent offline use for real server responses; fail closed, do not strip/re-sign data. .NET7 is an observed target, not a claim of continued vendor support or recommended maintained deployment. ## Install and run ```sh dotnet restore Desktop.csproj dotnet build Desktop.csproj dotnet run --project Desktop.csproj ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ```text # Go CLI Sandbox application Requirements: Go1.22+, standard library. The complete project archive includes the unchanged tested samples/sdks/go tree alongside samples/apps/go-cli. The entry point builds and runs the native Go client, never a Python/Node subprocess. Preserve that relative layout. Third-party runtime modules are not needed. ## Install and run ```sh sh run.sh status sh run.sh activate sh run.sh check sh run.sh validate sh run.sh deactivate ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ```text # Java console Sandbox application Requirements: Java17+, Gson2.14.0, Bouncy Castle1.86. The complete archive preserves samples/apps/java-console, samples/sdks/java and samples/security/java. Its shell entry uses the exact native JVM client and checksum-pinned build, not an invented registry SDK. Windows uses semicolon classpaths if running the Java command directly. No Python/Node runtime is required. ## Install and run ```sh sh run.sh status sh run.sh activate sh run.sh check sh run.sh validate sh run.sh deactivate ``` Set KDP_APP_ID and an absolute private KDP_STATE_DIR through trusted configuration. Official SDK apps use KDP_BASE_URL as a site origin; Go/Java REST apps use the API base ending /api/v1 and require KDP_TRUSTED_KEY_FILE pointing to an independently trusted public-key document. Default is the official HTTPS service. Explicit local tests may use KDP_ALLOW_LOOPBACK=1; never arbitrary HTTP. Use only a real Sandbox key (TEST prefix) and its matching email when supplying email. GUI apps collect these through fields; CLI apps read KDP_LICENSE_KEY/KDP_CUSTOMER_EMAIL privately from the environment. Never commit those values or use an lmp_ developer API key in an installed app. ## Demonstration sequence 1. Check fresh status or attempt a check. Expected: not activated; no feature access. 2. Activate with matching Sandbox data. Expected: accepted signed result or a clear refusal. 3. Check again. Expected: verified cache where eligible or online verification, never token-presence permission. 4. Validate online. Expected: actual signed decision; explicit server rejection never becomes offline permission. 5. Simulate transport loss only in an isolated test. Expected: accepted signed offline state inside grace, or fail closed when absent/invalid/expired. Current SDK limitations remain documented. 6. Test PDF export. Expected: enabled only after valid/signature/feature checks; PHP/WPF recheck on the action. The demonstration creates no invoice/payment or real PDF. 7. Deactivate. Expected: seat release, no feature grant and no token/cache left; stable installation identity remains. ## Privacy, recovery and limitations Use private per-application state, serialize operations and preserve installation identity across updates. Files protected by permissions are not automatically encrypted OS vaults. Do not expose a raw token, SDK object, signed customer payload or arbitrary storage path through UI/IPC/HTTP. Never bypass a signature failure to make an offline example pass. No mutation is automatically retried by these application entry points. A lost activation/release response needs reconciliation before retrying. Deactivation does not reset cumulative activation_count. Removing the app/package alone does not release a server seat. Deactivate online first and follow an explicit retention policy for exact app state. Test inputs/refusals/outages are isolated Sandbox data, not Live payments or a fabricated success. Native macOS execution/cross-compilation boundaries and the unchanged SDK canonicalization/cache defects are recorded in the documentation evidence. Full guide: https://licensing.kdpika.com/docs/v1/sdks/sample-apps ``` ## Common demonstration flow 1. Start fresh and check. Expected: NOT_ACTIVATED or a friendly refusal; token-presence status alone never enables a feature. 2. Enter a wrong email for the genuine Sandbox key. Expected: INVALID_LICENSE and no feature access. 3. Activate with matching data. Expected: real signature/binding/policy acceptance or a documented safe failure. 4. Restart/check with the same state. Expected: stable identity and eligible cache or online verification. 5. Validate online, then inject only a transport outage in an isolated test. Expected: bounded accepted signed state or honest refusal according to that actual SDK/client. 6. Inject an explicit HTTP refusal. Expected: no stale offline permission, regardless of earlier true features. 7. Use the protected PDF demo. Expected: validity/signature/feature gate; PHP/WPF recheck on the server/controller action. No real payment, invoice or PDF is generated. 8. Deactivate. Expected: confirmed release, cleared token/cache and no feature grant. ## Storage, UI and deployment safety Electron owns the SDK in its main process and checks the IPC sender/frame; context isolation and sandboxing stay enabled. Renderer code receives only safe decisions. PHP owns the SDK on the server and protects state-changing forms with actual session/CSRF checks. Its access code is for local testing, not a production login design. Serialize one installation state writer, keep identity stable and store state outside the app installation/ASAR/public web tree. File permissions are not automatically an OS vault. Do not package test secrets, arbitrary-path IPC, a test-only clock or a populated cache. The exact README explains each runtime/platform requirement. A visible button is never authorization. Recheck valuable server-side actions through your application auth/tenant/security layer. An attacker controlling a local app can patch it; do not promise unbreakable desktop protection. ## Verified results and honest limits The documentation evidence records actual source and packaged Electron windows, Python source/frozen CLI, PHP browser/CSRF workflow, C# controller/API tests and cross-build, and real Go/Java CLI runs. Local loopback forwarders deliberately inject outages/refusals; no production/provider outage, [Live](/docs/v1/start/glossary#live) key or payment is used. Automated native macOS runs do not establish Windows/Linux GUI execution, mobile vault behavior, notarization or formal accessibility/security certification. WPF is Windows-only and is handled under the explicit compile-plus-controller R18 boundary. Current SDK signing/cache incompatibilities stay disclosed; this documentation release does not fix them silently. The recorded Electron dependency audit reports 8 moderate, 0 high and 0 critical development-tree findings, and 0 production-tree findings. The moderate advisory propagates through packaging dependencies; this is not a zero-vulnerability certification. Run npm audit before distributing your own build and review tested dependency updates. These development tools are not installed on the PHP hosting server. ## Expected result Your integration authenticates signed state and handles failures without granting features. Installation identity, source version and test boundaries remain explicit. ## Common mistakes - Running a local demo router as a public production identity provider. - Shipping a populated cache, customer key or [developer API key](/docs/v1/start/glossary#developer-api-key). - Treating UI button state or release success as permission. ## Security notes Keep [developer](/docs/v1/start/glossary#developer) keys on trusted backends, pin independent key trust, preserve HTTPS/signature verification and never equate token presence with permission. Local machine owners can patch software; a successful build or checksum is not an anti-tamper certification. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | App starts but no activation | Missing App ID/state configuration | Follow the exact project README and use real Sandbox values | | C# offline refuses | Known real Unicode cache limitation | Show the refusal/online recovery; never bypass signatures | | App update creates another installation | State stored in an ephemeral installation/extraction path | Use a stable private per-application state root | ## Next steps Review [the SDK downloads](/docs/v1/sdks/overview), [test vectors](/docs/v1/security/test-vectors) and [API errors](/docs/v1/api/errors) for the exact selected client. ## Related - [Installation identity](/docs/v1/security/installation-id) - [Secure token storage](/docs/v1/security/secure-token-storage) - [API conventions](/docs/v1/api/conventions) # SDK and runtime compatibility Canonical: https://licensing.kdpika.com/docs/v1/sdks/compatibility --- title: "SDK and runtime compatibility" description: "Compare real SDK versions, runtimes, application surfaces, storage behavior, known limitations and verified platform boundaries." slug: "/docs/v1/sdks/compatibility" section: "SDKs and languages" order: 63 audience: "all" level: "advanced" last_updated: "2026-10-21" version: "v1" related: ["/docs/v1/sdks/overview", "/docs/v1/security/test-vectors"] draft: false --- Choose by actual runtime and trust behavior, not an unverified universal-parity claim. ## What you will learn - Compare all four [official SDKs](/docs/v1/start/glossary#official-sdk) with ten REST projects. - Understand native OS, cache, [feature](/docs/v1/start/glossary#feature) and vault boundaries. - Plan a controlled source/dependency upgrade. ## Prerequisites - Complete [the Sandbox quickstart](/docs/v1/start/quickstart). - Read [signed responses](/docs/v1/security/signed-responses), [authentication](/docs/v1/start/authentication) and [offline mode](/docs/v1/security/offline-mode). ## Current source versions | Language | Observed version | Declared target | Actual requirement / boundary | Application surface | | --- | --- | --- | --- | --- | | python | 1.1.5 | >=3.11,<3.15 | Python 3.11–3.14 | PyPI package; import kdpika_licensing | | php | 1.0.0 | ^8.1 | PHP8.2+ (readonly result class) | Composer PSR-4 backend library | | javascript | 1.0.0 | >=18 | Node18+; test each deployed runtime | module; Node/Electron main process only | | csharp | 1.0.0 | net7.0 | .NET7+ compatible target; prefer maintained runtime | C# project reference; async client | Version1.0.0 identifies the observed source package; PHP uses the existing versioned download label, not a Composer version field. Python 1.1.5 is now published on [PyPI](https://pypi.org/project/kdpika-licensing/) with [GitHub source](https://github.com/kdpika/kdpika-licensing); other packages remain source downloads. No support-duration SLA is implied. Source ZIP SHA-256 values on the overview are the package integrity source. ## Actual runtime verification All four official source packages have clean local installs and current package test entries. Python, PHP, Node and C# execute genuine isolated Sandbox lifecycle programs. The current package test suite includes a new PHP package-local entry with the same assertions; the unchanged legacy repository entry still assumes repository-root vendor/autoload.php. The ten REST projects compile/execute using pinned development dependencies, build from fresh exact ZIP extractions and run actual Sandbox checks. TypeScript uses Node22 for this complete example; that is not a change to the official Node SDK minimum. Go has no external runtime module. C/C++ use an explicit POSIX core; Swift here is macOS/CryptoKit, not a claimed universal target. ## Operating systems and UI The available native host is macOS. Portable runtimes can target other supported systems, but Windows/Linux/mobile execution is not inferred from a macOS build. WPF requires Windows: its full project cross-builds with Windows targeting and its shared controller executes on the local Sandbox, but its native UI is not executed on macOS. Electron source and unsigned macOS packaged-app runs are separate verified cases. Python CLI also runs as a frozen PyInstaller macOS build. Code signing, notarization, Windows/Linux packaging, assistive-technology certification and real OS-vault backends remain separate deployment responsibilities. ## Storage and feature semantics Python can prefer an available positive-priority keyring, with permission-protected file fallback. PHP, Node and C# do not automatically provide Keychain/DPAPI/Secret Service adapters. See the tested storage guidance before claiming encrypted-vault protection. Every feature helper must follow a valid/signature gate. Helpers differ in truthiness/fallback semantics, especially C# nonboolean values; numeric quotas require explicit type/range checks. A successful deactivation has valid=true for the release operation but no entitlement grant. activated/token presence is not current [license](/docs/v1/start/glossary#license) validity. ## Known compatibility gaps | Current source | Observed issue | Safe application response | | --- | --- | --- | | Legacy Python 1.0.0 | Separator/exponent canonicalization defects | Upgrade to Python SDK 1.1.5; retain all signature and binding checks | | JavaScript official SDK | Integral-float spelling such as19.0 can be lost | Deny invalid signature; the REST TypeScript example uses the separate lossless P5 verifier | | C# | Cached Unicode serialization can invalidate a real signed state | Online recovery may work; genuine offline cache failure is not falsely reported as success | | PHP | ^8.1 metadata conflicts with readonly class requiring8.2; PHP8.5 curl_close notice | Use the actual source runtime and handle deprecation separately; never suppress licensing validation | | Legacy/source SDKs | Feature helpers alone may not enforce result validity | Gate valid and signature status before feature use | | Signing keys | No historical key ring/overlap promise supplied | Pin trust deliberately and coordinate updates | These are documented source defects/limits, not approvals to modify SDK behavior. Runtime implementations, APIs and protected [product](/docs/v1/start/glossary#product) files stay unchanged by this documentation release. ## Compatibility policy API v1 is independent of SDK package versions. Pin exact source/dependency locks and test current response/cache/clock/refusal behavior before updating. A file named1.0.0 does not establish support for every historical release. Additional fields should not be stripped from signed payloads; signature canonicalization must include the complete signed payload. Treat constructor/API changes, namespace/identity changes, signed-byte behavior, token format and offline-time rules as compatibility-sensitive. No minimum deprecation notice, support window or automatic old-key overlap is promised where the actual product does not implement one. ## Choose and verify your target 1. Match the actual runtime and application surface in the table. Expected: a library/client suitable for your trusted process. 2. Download and verify the exact source archive. Expected: SHA-256 matches the overview. 3. Follow the complete guide in a clean application project. Expected: pinned dependency restore/build and matching API surface. 4. Run real signed [activation](/docs/v1/start/glossary#activation), cache reload, offline/refusal and release. Expected: explicit accepted decisions or safe refusals, not assumptions from successful import. 5. Repeat after packaging on your actual target OS. Expected: native proof for your distribution, without claiming other targets were executed. ## Expected result Your integration authenticates signed state and handles failures without granting features. Installation identity, source version and test boundaries remain explicit. ## Common mistakes - Assuming a declared runtime minimum proves native execution on every OS. - Treating all feature helper fallbacks as identical. ## Security notes Keep [developer](/docs/v1/start/glossary#developer) keys on trusted backends, pin independent key trust, preserve HTTPS/signature verification and never equate token presence with permission. Local machine owners can patch software; a successful build or checksum is not an anti-tamper certification. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | PHP8.1 parse failure | Actual readonly class requires8.2 | Use the actual supported source runtime | | C# offline fails after restart | Known Unicode cache byte mismatch | Fail closed and reconnect; do not strip signed fields | | Unknown package registry release | No public registry publication established | Install the exact source ZIP and verify its checksum | ## Next steps Review [the SDK downloads](/docs/v1/sdks/overview), [test vectors](/docs/v1/security/test-vectors) and [API errors](/docs/v1/api/errors) for the exact selected client. ## Related - [Installation identity](/docs/v1/security/installation-id) - [Secure token storage](/docs/v1/security/secure-token-storage) - [API conventions](/docs/v1/api/conventions) # Migrating between SDK versions Canonical: https://licensing.kdpika.com/docs/v1/sdks/upgrading --- title: "Migrating between SDK versions" description: "Upgrade or reinstall the current SDK source safely while preserving identity, trust, storage, cache policy and rollback evidence." slug: "/docs/v1/sdks/upgrading" section: "SDKs and languages" order: 64 audience: "all" level: "advanced" last_updated: "2026-10-21" version: "v1" related: ["/docs/v1/sdks/overview", "/docs/v1/security/test-vectors"] draft: false --- Upgrade the application package without accidentally replacing its installation identity or weakening [license](/docs/v1/start/glossary#license) verification. ## What you will learn - Use a complete source/runtime/API migration checklist. - Reinstall the verified current source without invented version history. - Handle rollback, uncertain operations and intentional uninstall safely. ## Prerequisites - Complete [the Sandbox quickstart](/docs/v1/start/quickstart). - Read [signed responses](/docs/v1/security/signed-responses), [authentication](/docs/v1/start/authentication) and [offline mode](/docs/v1/security/offline-mode). ## Current version and history boundary The current Python release is **kdpika-licensing 1.1.5**, published on [PyPI](https://pypi.org/project/kdpika-licensing/) with MIT-licensed [GitHub source](https://github.com/kdpika/kdpika-licensing). Other source SDK labels remain 1.0.0. ### Python migration from studio-soline-license-sdk 1. Stop processes using the old distribution and record your existing [product](/docs/v1/start/glossary#product), environment, application name and private state directory. 2. Preserve installation identity and protected state. Do not delete either to upgrade. 3. Use Python 3.11–3.14. If installed in this environment, remove the old studio-soline-license-sdk distribution before installing the new one. 4. Install kdpika-licensing 1.1.5 and change imports from license_sdk to kdpika_licensing. 5. Check your pinned verification-key ID matches the Ed25519 public-key hash. New validation rejects mismatched pins rather than silently accepting them. 6. Test signed [activation](/docs/v1/start/glossary#activation), validation, cache reload, offline deadlines and refusal paths before distributing your app. ```bash python -m pip uninstall studio-soline-license-sdk python -m pip install "kdpika-licensing==1.1.5" python -m pip check ``` Run the uninstall only if the old distribution is installed in that environment. The deprecated license_sdk import remains a compatibility shim, not the recommended import. Do not keep two distributions owning that namespace. Review the [versioned migration guide](https://github.com/kdpika/kdpika-licensing/blob/v1.1.5/docs/migration.md). API v1 and SDK versions are independent. Download the actual source package and SHA-256 from the SDK overview. Keep a private application dependency lock and the current runtime package checksum. Do not replace the first-party package with an unverified registry name. ## Migration review template | Review item | Record before | Verify after | | --- | --- | --- | | Package identity | Exact language/name/version/hash | Intended source, no unexpected import/namespace change | | Runtime/dependencies | Compiler/runtime versions and dependency lock | New minimums actually work on the target | | Public API | Constructor/method signatures, result fields and exceptions | Application builds and expected safe failures remain handled | | Trust | Independently pinned public key/key_id | No untrusted replacement or disabled verification | | Installation | Actual stable identity and namespace inputs | Same installation hash and no accidental new activation | | Storage | Exact per-app location and vault/fallback category | Private permissions, readable expected state and atomic writes | | Signed bytes | Current response/cache vectors | Complete payload authenticates without numeric/Unicode drift | | Offline | Signed validation/grace/expiry/checkpoint behavior | Cache reload, network loss, rollback and explicit rejection tested | | [Features](/docs/v1/start/glossary#feature) | Boolean/numeric type handling and validity guard | Invalid result cannot unlock any feature | | Mutation recovery | Current activation/release state | No blind retry or seat/cumulative-count assumption | | Packaging | Real target artifact/signing settings | Actual native packaged app passes the same flow | ## Reinstall the current source safely 1. Record the exact current package/dependency/runtime versions and your application configuration. Expected: a reproducible private manifest without keys/tokens/[customer](/docs/v1/start/glossary#customer) payloads in shared logs. 2. Identify the exact private application state directory and stable namespace inputs. Expected: you know what must persist; no broad user-directory deletion command. 3. Back up required state using your application's actual retention/security policy. Expected: protected backup access, not a token pasted into a support ticket. 4. Download and verify the intended source ZIP checksum. Expected: exact current digest, otherwise stop. 5. Restore dependencies in a separate clean application project. Expected: the documented import/build succeeds before changing an installed application. 6. Run the full Sandbox lifecycle and published vectors. Expected: accepted signed decisions and safe refusals, with known release limitations visible. 7. Package and run on the actual target OS. Expected: the final artifact—not just the source interpreter—passes activation/check/refusal/offline/release behavior. 8. Update the deployed application without changing App ID/environment/origin/storage namespace accidentally. Expected: the existing installation hash persists and cumulative activation_count is not consumed unnecessarily. 9. Monitor safe error/source/request-ID metadata. Expected: no raw [license key](/docs/v1/start/glossary#license-key), token, private path, stack trace or customer payload in logs. This is a source package upgrade process, not authorization to modify platform billing, database state, API response shapes or SDK behavior. ## Identity and cache compatibility Changing origin, product, environment, application storage root or default application name can select another namespace and generate another [installation ID](/docs/v1/start/glossary#installation-id). Preserve the correct explicit state location across updates. Deactivation releases a seat but does not reset cumulative activation_count. A reinstall that deletes identity may consume another activation even after release. Do not copy one buyer's token/cache into another installation. Do not edit a signed payload to make a new client accept it. If a new release changes canonicalization or cache serialization, test actual raw server responses and restart/offline behavior before rollout. Fail closed on incompatible data and reconcile with the software provider. ## Rollback and uncertain operations Keep the old application artifact, exact dependency lock and required private state backup. A rollback is safe only when the earlier artifact can read the state format and preserve current trust/time semantics. Do not assume restoring a full machine snapshot is neutral for rollback checks or a server token already deactivated/revoked. If activation or deactivation lost its response, reconcile server state before retrying. Do not delete local state to hide uncertainty. Removing a package does not release its server seat. Releasing a seat while online before intentionally removing an installation is different from a normal in-place application upgrade. ## Uninstall deliberately Use the language-specific package/reference removal command only inside the consuming application project. A direct PHP copied SDK directory is not a Composer dependency-removal action. Keep or remove the exact application state according to your application's privacy/retention policy; do not recursively delete a broad home or workspace directory. Before removal, deactivate online if you intend to release the installation seat. Expected: confirmed release, no feature grant and identity preserved if the application policy retains it. If offline, show a simple reconnect/support path rather than pretending the server seat was released. ## Future version change entry For every future version, record the observed release/source commit and checksum, exact API/constructor changes, dependency/runtime requirements, data/trust migration steps, rollback limits, known defects and executed test evidence. If nothing changes, say exactly that with source evidence. Do not invent historic versions, a support deadline or a deprecation email/header mechanism absent from the product. ## Expected result Your integration authenticates signed state and handles failures without granting features. Installation identity, source version and test boundaries remain explicit. ## Common mistakes - Deleting identity/cache as a routine upgrade step. - Assuming package removal releases a server seat. - Rollback to an artifact that cannot read the current state/trust format. ## Security notes Keep [developer](/docs/v1/start/glossary#developer) keys on trusted backends, pin independent key trust, preserve HTTPS/signature verification and never equate token presence with permission. Local machine owners can patch software; a successful build or checksum is not an anti-tamper certification. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | New activation after upgrade | Namespace changed | Restore the intended origin/App ID/environment/state root | | Signature failures after update | Changed byte/Unicode/number handling | Run exact vectors and raw response/cache tests | | Uninstall left a seat | Package removal is not server deactivation | Reconnect and release deliberately or reconcile with the provider | ## Next steps Review [the SDK downloads](/docs/v1/sdks/overview), [test vectors](/docs/v1/security/test-vectors) and [API errors](/docs/v1/api/errors) for the exact selected client. ## Related - [Installation identity](/docs/v1/security/installation-id) - [Secure token storage](/docs/v1/security/secure-token-storage) - [API conventions](/docs/v1/api/conventions) # Set up webhooks Canonical: https://licensing.kdpika.com/docs/v1/guides/webhooks-overview --- title: "Set up webhooks" description: "Create isolated signed notification endpoints, store their secrets and test delivery without using notifications as license authority." slug: "/docs/v1/guides/webhooks-overview" section: "Webhooks and integrations" order: 65 audience: "all" level: "beginner" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/webhook-events","/docs/v1/guides/webhook-delivery"] draft: false --- Receive signed notifications on your backend when [licenses](/docs/v1/start/glossary#license) or [devices](/docs/v1/start/glossary#device) change. ## What you will learn - Add and test an HTTPS endpoint from your workspace. - Separate [Sandbox](/docs/v1/start/glossary#sandbox) and [Live](/docs/v1/start/glossary#live) deliveries. - Pause [delivery](/docs/v1/start/glossary#delivery) and coordinate signing-secret replacement. ## Prerequisites - A [workspace and product](/docs/v1/start/quickstart) with available [webhook](/docs/v1/start/glossary#webhook) allowance. - An HTTPS backend URL with a valid certificate and public DNS/IP address. - A receiver that reads raw request bytes and verifies [HMAC signatures](/docs/v1/guides/verify-webhook-signatures). - A server-side secret store outside your public web directory. ## Decide what the endpoint does Use notifications for activity views, support alerts, reconciliation and your own background workflows. Never unlock an application solely because a license.created or license.restored notification arrived. For application access, use the [activation and validation flow](/docs/v1/start/activation-validation-deactivation) and verify its signed response. This guide covers [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) sending events to you. A store integration runs in the other direction: your backend receives the store's authenticated payment event and calls the licensing API. The two endpoint secrets and verification algorithms are not interchangeable. ## Create an endpoint 1. Sign in to your workspace and select Sandbox in the environment control. 2. Open Webhooks in the sidebar. 3. Select Add endpoint. 4. Enter your final HTTPS endpoint URL, without a redirect, URL credentials or a secret query parameter. 5. Select one product or All products. 6. Select the events your handler implements from the [event catalog](/docs/v1/guides/webhook-events). 7. Select Create endpoint. 8. Copy the displayed Webhook signing secret into your backend's secret store immediately. 9. Pin that receiver to the sandbox envelope environment. Expected result: the endpoint appears as Active and the secret is shown once. The secret cannot be displayed again after that flash message is consumed; rotate it if you lost it. The new-endpoint dialog initially selects license.created, license.activated and license.revoked; choose deliberately rather than relying on those defaults. The URL is limited to 2048 characters. Private, loopback, reserved-IP, credential-bearing and redirecting targets cannot be used for platform delivery. You can update an endpoint URL with Update in its row. Already queued deliveries retain their original event IDs and are sent to the current URL at their next attempt. There is no general public REST endpoint for administering webhook endpoints. ## Test the connection 1. Keep the endpoint Active. 2. Select Send test in its row. 3. Find test.ping and its event ID in Delivery history. 4. Confirm your receiver authenticated the exact bytes and durably recorded that event ID. 5. Check the history row for a 2xx response and Delivered status. Expected result: test.ping arrives without creating a license, charging a payment or consuming an installation. Send test can target the endpoint even if test.ping was not selected among its business events. If the endpoint is disabled, the test can be queued but no network attempt occurs yet. A confirmation saying sent or queued is not proof of receipt. ## Separate environments Create separate endpoint records and secrets for Sandbox and Live. Ordinary event routing matches the endpoint's [organization](/docs/v1/start/glossary#organization), environment, product [scope](/docs/v1/start/glossary#scope) and selected event names. Do not infer the environment from a hostname or a key prefix alone; verify the signed environment value as well. A Free workspace is for Sandbox evaluation. A webhook notification does not grant Live access or bypass [platform limits](/docs/v1/api/rate-limits). Before enabling Live, run the [go-live checklist](/docs/v1/start/go-live-checklist). ## Disable and enable 1. Select Disable in the endpoint row before maintenance. 2. Confirm the endpoint status becomes Disabled. 3. Finish your receiver maintenance. 4. Select Enable. 5. Inspect remaining queued deliveries and retry eligible failed rows when necessary. Expected result: existing queued rows are retained and attempts are skipped while disabled; attempts are not incremented by those skips. Ordinary events published while disabled are not queued for that endpoint. Enabling is not a historical backfill. A previously delivered row does not send again. See [delivery limits](/docs/v1/guides/webhook-delivery) before relying on Retry. ## Rotate a signing secret 1. Prepare a secure way to update the receiver's secret configuration. 2. Disable the Sandbox endpoint to reduce new attempts during the change. 3. Select Rotate and confirm replacement. 4. Copy the new one-time secret into your receiver configuration. 5. Reload the receiver using your normal secure deployment process. 6. Enable the endpoint. 7. Send test and confirm authenticated receipt. 8. Remove retired receiver credentials after accounting for any request already in flight. Expected result: new attempts use the replacement secret. Rotation takes effect immediately on the sender, including queued events. There is no automatic old/new overlap, secret-version header or two-active-secret rotation API. A request already sent before rotation may still be in flight; coordinate that window and use durable deduplication. The temporary pause does not queue business events published while disabled, so reconcile any gap from your own backend records. ## Expected result Your backend receives only its configured notifications, rejects invalid signatures and environments, records authenticated event IDs durably and does not treat them as application license authorization. ## Common mistakes - Putting a [developer API key](/docs/v1/start/glossary#developer-api-key) or endpoint signing secret in a desktop executable or browser application. - Using a Stripe signing secret for a KDPika Licensing notification. - Treating Enable as a replay of all events missed while disabled. - Assuming Send test bypasses Disabled status. ## Security notes Use one endpoint secret per record. Do not log the secret, complete signature header, [license key](/docs/v1/start/glossary#license-key) or full [customer](/docs/v1/start/glossary#customer) payload. No fixed outbound IP range is published here; authenticate the signature instead of inventing an IP allowlist. Keep your own event log private and apply a justified retention policy. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Endpoint creation is refused | Invalid URL, no selected event, unavailable product, environment or allowance | Check the form message and product/environment selection | | Test stays Pending | Disabled endpoint or no maintenance run yet | Enable it, then inspect Delivery history | | Authentication fails after Rotate | Receiver still uses the retired secret | Update its secure configuration and test again | | A product event is absent | Product, environment or event subscription does not match | Check all three routing settings before retrying | ## Next steps Read the [full event catalog](/docs/v1/guides/webhook-events), then implement [delivery handling](/docs/v1/guides/webhook-delivery). ## Related - [Verify webhook signatures](/docs/v1/guides/verify-webhook-signatures) - [Test webhooks locally](/docs/v1/guides/test-webhooks-locally) - [Webhook security](/docs/v1/security/webhook-security) # Webhook event catalog Canonical: https://licensing.kdpika.com/docs/v1/guides/webhook-events --- title: "Webhook event catalog" description: "All eleven notification events, captured Sandbox payloads, field definitions, triggers and safe receiver actions." slug: "/docs/v1/guides/webhook-events" section: "Webhooks and integrations" order: 66 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/webhooks-overview","/docs/v1/guides/webhook-delivery"] draft: false --- Use this catalog to recognize notifications that [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) actually emits. ## What you will learn - Identify the trigger and fields for every event. - Separate notifications from authoritative [license](/docs/v1/start/glossary#license) validation. - Handle known gaps without inventing additional event types. ## Prerequisites - Complete [endpoint setup](/docs/v1/guides/webhooks-overview). - Implement [signature verification](/docs/v1/guides/verify-webhook-signatures). - Read [license lifecycle rules](/docs/v1/start/license-lifecycle). ## Envelope shared by every event | Field | Type | Meaning | | --- | --- | --- | | id | string | Event ID beginning evt_; not a UUID contract. Stable across [delivery](/docs/v1/start/glossary#delivery) retries. | | event | string | Name from this catalog. | | created_at | string | UTC ISO 8601 notification-creation time, not the delivery attempt time. | | environment | string | [sandbox](/docs/v1/start/glossary#sandbox) or [live](/docs/v1/start/glossary#live); pin the expected value in your receiver. | | data | object | Event-specific fields described below. | The payloads below were captured from real local Sandbox service and maintenance operations on 2026-10-14. All identifiers and values are synthetic. They were queued locally, not delivered by a third-party service. Formatting here is for readability; verify the original incoming HTTP bytes, not this displayed JSON formatting. Only subscribed active endpoints in the same [organization](/docs/v1/start/glossary#organization) and environment receive ordinary events. A product-scoped endpoint receives its [product](/docs/v1/start/glossary#product); an All products endpoint receives eligible events across that environment. The envelope does not include an organization ID or product ID: your endpoint configuration and your saved license mapping establish that context. Do not assume resource ownership from a supplied license ID alone. ## license.created A successful license creation commits, then queues this notification. It contains a masked key, never the complete key. ```json { "id": "evt_6db5fc08a2fdc0664fd58904", "data": { "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "active", "expires_at": "2026-11-13T17:43:50+00:00", "license_key_mask": "TESTDOCS-••••-••••-7CCL" } }, "event": "license.created", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Created license ID. | | `license.status` | string | active at creation. | | `license.expires_at` | string or null | UTC expiration or null for an unbounded license. | | `license.license_key_mask` | string | Display-only key mask; cannot activate a license. | Recommended action: Record the license ID in your server-side order mapping. Do not issue another license or attempt to email the mask as a key. ## license.activated A successful [activation](/docs/v1/start/glossary#activation), including a new [activation token](/docs/v1/start/glossary#activation-token) for an existing installation, queues this notification. ```json { "id": "evt_c8213ce9e0751fcbb2079e82", "data": { "device": { "id": "dev_af3087ec2eeb536a4f4c61a7" }, "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "active" } }, "event": "license.activated", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Activated license ID. | | `license.status` | string | active at activation. | | `device.id` | string | Server device-record ID, not the [installation ID](/docs/v1/start/glossary#installation-id) you supplied. | Recommended action: Update installation activity in your backend. Authorize application [features](/docs/v1/start/glossary#feature) using the validated signed response, not this notification. ## license.deactivated Client deactivation or a [device](/docs/v1/start/glossary#device) release from the [customer](/docs/v1/start/glossary#customer) portal queues this notification. Deactivating a device from the [developer](/docs/v1/start/glossary#developer) Devices screen does not emit this event. ```json { "id": "evt_47da0040c780d987b4662467", "data": { "device": { "id": "dev_af3087ec2eeb536a4f4c61a7" }, "license": { "id": "lic_18d0daa92fddced51a44ed14" } }, "event": "license.deactivated", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Affected license ID. | | `device.id` | string | Released server device ID. | | `source` | string, optional | customer_portal for the portal release path; absent on client deactivation. | Recommended action: Update your device activity view. Do not infer that every possible device change generates this notification. ## license.suspended An allowed suspension queues this event. Repeating suspension can create a new event even when the license was already suspended. ```json { "id": "evt_863acadaee69a2a86662efc1", "data": { "reason": "", "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "suspended" } }, "event": "license.suspended", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Affected license ID. | | `license.status` | string | suspended. | | `reason` | string or null | Operation reason; can be an empty string. | Recommended action: Notify your support workflow and refresh your backend license view. Do not run another suspension in response to the same notification. ## license.restored The suspended-to-active transition queues this event. Setting an already active license to active does not produce license.restored. ```json { "id": "evt_0f5d4d8a4ee2ab283d1822bc", "data": { "reason": "", "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "active" } }, "event": "license.restored", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Restored license ID. | | `license.status` | string | active. | | `reason` | string or null | Operation reason; can be an empty string. | Recommended action: Refresh current license state. Previously revoked activation tokens do not become usable merely because restoration was reported; the application may need to activate again. ## license.revoked An allowed revocation queues this event. Repeating revocation can create another event ID. ```json { "id": "evt_0d29f4ae752612f98e63f18e", "data": { "reason": "Synthetic test", "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "revoked" } }, "event": "license.revoked", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Revoked license ID. | | `license.status` | string | revoked. | | `reason` | string or null | Operation reason; can be an empty string. | Recommended action: Flag the order/license mapping for support. Revocation is terminal in the current lifecycle; do not promise automatic restoration after a reversed refund or won dispute. ## license.renewed A successful renewal queues this event. The API adds days to a future expiration, or to the current time if the expiration has passed or is absent. ```json { "id": "evt_25995883974e153db0cf8860", "data": { "days": 365, "license": { "id": "lic_18d0daa92fddced51a44ed14", "status": "active", "expires_at": "2027-11-13T17:43:50+00:00" } }, "event": "license.renewed", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Renewed license ID. | | `license.status` | string | State after renewal; a suspended license stays suspended. | | `license.expires_at` | string | New UTC expiration. | | `days` | integer | Days added, from 1 through 3650 inclusive. | Recommended action: Record the resulting expiration and reconcile it with your store ledger. Never call renew again merely because this notification is delivered twice. ## license.expiring A maintenance run selects active licenses due within seven days and emits reminders when the remaining rounded-up whole days equal seven or one. The reminder marker is license ID plus day count, not expiration revision. ```json { "id": "evt_c237ff96e67f2e36ecb4f0ad", "data": { "license": { "id": "lic_f8f3017881fdbc8307163c79", "expires_at": "2026-10-15T17:46:36+00:00", "days_remaining": 1 } }, "event": "license.expiring", "created_at": "2026-10-14T17:46:36+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | License approaching expiration. | | `license.expires_at` | string | UTC expiration. | | `license.days_remaining` | integer | Rounded-up reminder bucket: 7 or 1. | Recommended action: Schedule a customer reminder with your own deduplication and consent policy. After renewal the same reminder bucket can be suppressed by the existing marker; reconcile scheduled communications independently. ## license.expired A maintenance run persists an elapsed active or suspended license as expired and expires its active activation tokens. A rejected API validation by itself does not guarantee this event or the stored state transition. ```json { "id": "evt_4c8cbd02e77bfc62c54484c4", "data": { "license": { "id": "lic_f8f3017881fdbc8307163c79", "status": "expired" } }, "event": "license.expired", "created_at": "2026-10-14T17:46:36+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | Expired license ID. | | `license.status` | string | expired. | Recommended action: Refresh the license view and offer your own renewal route if supported. A delayed notification does not extend paid access or SDK [offline grace](/docs/v1/start/glossary#offline-grace). ## device.reset A developer resets all [active devices](/docs/v1/start/glossary#active-devices) for a license. Active device rows are deactivated and active activation tokens are revoked. ```json { "id": "evt_eb4ce6fbdcc85b9e5384e935", "data": { "license": { "id": "lic_18d0daa92fddced51a44ed14" } }, "event": "device.reset", "created_at": "2026-10-14T17:43:50+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `license.id` | string | License whose devices were reset. | Recommended action: Tell your application/support workflow that reactivation may be required. This event does not contain a complete device list. ## test.ping Send test queues a synthetic notification for the selected endpoint even if test.ping is not in its business-event subscription. A disabled endpoint can queue it but does not send it until enabled. ```json { "id": "evt_ffa11b785e0ede73d9e69801", "data": { "message": "Signed webhook test", "webhook_id": "whk_2bf9c0559190a2101b3b9d1b" }, "event": "test.ping", "created_at": "2026-10-14T17:46:36+00:00", "environment": "sandbox" } ``` | Field inside data | Type | Meaning | | --- | --- | --- | | `message` | string | Signed [webhook](/docs/v1/start/glossary#webhook) test. | | `webhook_id` | string | Selected endpoint ID. | Recommended action: Confirm signature verification and durable receipt only. Do not issue, renew or unlock a license. ## Events that do not exist There is no license.validated, plan.changed, customer.created, email.delivered or store-payment event in this outbound contract. Device deactivation from the developer Devices screen does not emit license.deactivated. A recurring-payment integration receives payment-provider events on your backend, calls the appropriate licensing API, then may receive the corresponding licensing notification. Do not create an event feedback loop. ## Expected result Every recognized event has an authenticated envelope, an explicit handler and a durable event-ID deduplication record. Unrecognized names are rejected or quarantined without a business action. ## Common mistakes - Treating a [license key](/docs/v1/start/glossary#license-key) mask as a usable key. - Assuming all events include customer email, plan, product or a full license object. - Extending a license again when license.renewed is redelivered. - Assuming a reminder or expiration notification arrives at an exact wall-clock instant. ## Security notes Event data can be delayed or arrive out of order. Use [get license](/docs/v1/api/get-license) on your authenticated backend when you need current resource details. Client feature access still requires a valid, verified signed activation/validation response. Treat reason text as untrusted display content and escape it in HTML, notifications and logs. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | No customer email in license.created | The contract only sends a key mask and license fields | Use your secured order/customer mapping; do not scrape logs for a key | | No reminder after a later renewal | Reminder deduplication is not keyed by expiration revision | Reconcile reminders from your own billing schedule | | No expiry event after rejected validation | Maintenance performs the persistent transition | Check signed/API validity now; do not wait for a notification | | Several activation events for one installation | Reactivation can rotate a token | Track server device ID and do not count events as new seats | ## Next steps Implement the [delivery and duplicate-handling rules](/docs/v1/guides/webhook-delivery), then run the [local receiver test](/docs/v1/guides/test-webhooks-locally). ## Related - [Webhook setup](/docs/v1/guides/webhooks-overview) - [Signed responses](/docs/v1/security/signed-responses) - [Idempotency limitations](/docs/v1/api/idempotency) # Webhook delivery and retries Canonical: https://licensing.kdpika.com/docs/v1/guides/webhook-delivery --- title: "Webhook delivery and retries" description: "Exact headers, HMAC bytes, attempt limits, delivery history, ordering, duplicate handling and manual retry limitations." slug: "/docs/v1/guides/webhook-delivery" section: "Webhooks and integrations" order: 67 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/webhook-events","/docs/v1/guides/verify-webhook-signatures"] draft: false --- Make your receiver safe when notifications are delayed, repeated or delivered out of order. ## What you will learn - Reconstruct the exact signature input. - Acknowledge durable receipt and deduplicate business actions. - Interpret [delivery](/docs/v1/start/glossary#delivery) states, automatic attempts and dashboard Retry. ## Prerequisites - An [active notification endpoint](/docs/v1/guides/webhooks-overview). - Its signing secret in your backend's secure configuration. - A durable database or queue under your control. ## Request headers and raw body The sender makes an HTTPS POST to the configured final URL. | Header | Value | Receiver action | | --- | --- | --- | | Content-Type | application/json | Read raw bytes before JSON middleware | | User-Agent | LMP-Webhooks/1.0 | Diagnostic identifier, not authentication | | X-LMP-Event-ID | evt_ followed by an opaque identifier | Match against authenticated body.id | | X-LMP-Event | Event name | Match against authenticated body.event | | X-LMP-Signature | t=decimal_unix_seconds,v1=lowercase_hex_hmac | Verify exact bytes with the endpoint secret | The LMP names above are compatibility identifiers. Keep them unchanged even though the [product](/docs/v1/start/glossary#product) is named [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing). The signed message is the ASCII decimal timestamp, one literal period byte, and the entire unmodified raw request body. The key is the plaintext endpoint signing secret; the algorithm is HMAC-SHA256 and the digest is lowercase hexadecimal with 64 characters. This is separate from the Ed25519 signature in a [license](/docs/v1/start/glossary#license) response. Parsing and re-encoding JSON, trimming whitespace, changing Unicode escaping or sorting keys before verification invalidates the signature. The body contains the [common event envelope](/docs/v1/guides/webhook-events#envelope-shared-by-every-event). The delivery timestamp is generated afresh for each attempt. The event ID, event.created_at and stored payload remain stable on retry. The current endpoint URL and current secret are used for the next attempt. ## Receiver timestamp policy The sender does not enforce your receiver's freshness policy. The [verification examples](/docs/v1/guides/verify-webhook-signatures) require the signature timestamp to be within 300 seconds of the receiver clock in either direction. They impose a 1 MiB body limit. Both numbers are example receiver policy, not promises about a sender inbound limit or a new licensing API rate limit. Synchronize the receiver clock. Check the signature timestamp, not event.created_at: a valid queued event can be older than 300 seconds when its next attempt is signed. A recent timestamp does not stop replay inside the acceptance window. Persist the authenticated event ID as well. ## Durable acknowledgement and duplicate handling 1. Read a bounded raw body. 2. Verify the signature, freshness, expected environment and matching event headers. 3. Look up your own resource/order mapping; do not trust a resource ID as authorization by itself. 4. Atomically insert a unique authenticated event ID and a durable work item in your own database. 5. Return a 2xx response after commit. 6. Process the work item using a separately deduplicated business-operation key. Expected result: a duplicate already accepted event returns a harmless 2xx without repeating a business effect. If durable storage fails, return a non-2xx response so delivery can retry. If an existing authenticated ID arrives with different body bytes, quarantine it for investigation rather than applying a second action. Do not return 200 before persistence, and do not wait for a slow email provider or store API call before acknowledgement. A lost 2xx response can produce a retry even when your first handler succeeded. Exactly-once processing is not provided by HTTP or by the licensing API. In particular, [create replay has concurrency and expiration limitations](/docs/v1/api/idempotency); renew is additive and not idempotent. Your inbox dedupe is necessary but does not make an uncertain downstream call safe to repeat. ## Automatic attempts | Failed attempt | Delay before the next eligible attempt | | --- | --- | | 1 | 60 seconds | | 2 | 300 seconds | | 3 | 1800 seconds | | 4 | 7200 seconds | | 5 | 43200 seconds | | 6 | None; automatic attempts stop | These delays are measured after failure handling. The maintenance scheduler processes due deliveries, so wall-clock delivery can be later than the minimum delay. At most six network attempts are made for one delivery row. A disabled endpoint or a row locked by another worker is skipped without consuming an attempt. Success requires an HTTP status from 200 through 299 and no transport error. A 204 is sufficient; there is no required success-response body. Redirects, client errors, rate limits, server errors and transport failures are unsuccessful. Retry-After from a receiver does not replace the sender's fixed retry schedule. ## Timeouts and network behavior | Control | Actual behavior | | --- | --- | | Connection timeout | 5 seconds | | Total request timeout | 12 seconds | | Redirect following | Disabled | | TLS certificate and hostname | Verified | | Target address | Public DNS/IP validation with the validated address pinned for that attempt | | Response body retained | At most 1000 bytes | | Each response header value retained | At most 500 characters | | Last error retained | At most 500 characters | Return small generic receiver responses. Do not include a secret, complete [license key](/docs/v1/start/glossary#license-key) or [customer](/docs/v1/start/glossary#customer) detail in an error response: an excerpt can be retained on the sender. A connection reset after your commit is still an uncertain acknowledgement and can cause a duplicate delivery. ## Ordering and identifiers There is no global ordering or exactly-once guarantee. A failed earlier event can arrive after a newer event succeeds. One business publication can use the same event ID for multiple subscribed endpoints; each has a separate delivery row. A repeated business operation can create a new event ID, so event-ID deduplication alone is not order/invoice deduplication. For a current license view, fetch [get license](/docs/v1/api/get-license) on your authenticated backend instead of blindly replacing it with an old event payload. For a side effect such as sending one purchase email, key the durable operation to your order/line/license mapping and the intended effect. Never let a delayed license.restored notification reverse a later revocation. ## Delivery history and failure states Open [Webhooks](/docs/v1/start/glossary#webhook) and read Delivery history. The page shows the latest 250 delivery rows, not an append-only row for each historical attempt. Each row displays creation time, event name/ID, endpoint host, most recent HTTP status and duration, cumulative attempts, status, last error and next attempt time. Absence of an old row does not prove delivery never happened. | Stored status | Meaning | | --- | --- | | PENDING | Queued, no completed network attempt yet | | FAILED with a next attempt and count below 6 | A failed attempt eligible for a later retry | | FAILED at count 6 | Attempt budget exhausted; no automatic recovery | | DELIVERED | A 2xx response without transport error was received | Delivered rows are eligible for maintenance deletion after 30 days. Failed and pending rows do not have that delivered-row cleanup condition. Retain only the minimal necessary receiver evidence under your own policy; dashboard history is not your billing ledger or backup. ## Manual Retry is not unrestricted replay 1. Fix your receiver problem. 2. Check that the endpoint is Active. 3. Find the failed row and its current attempt count. 4. If the count is below six, select Retry and inspect the new result. Expected result: an eligible row becomes due immediately and attempts once with its original event ID/body and a new signature timestamp. The attempt count is not reset. A Delivered row is not resent. :::warning Exhausted delivery limitation The interface can show Retry at six failed attempts, but that action does not restart the exhausted delivery. Do not rely on its confirmation wording as proof of another network attempt. Reconcile the affected license from the licensing API and your own durable records, then contact hi@kdpika.com if delivery investigation is needed. There is no public event-payload export or arbitrary historical replay API. Do not invent a replacement event or repeat an uncertain non-idempotent renewal. ::: You can replay a captured synthetic request locally after generating a fresh test signature. That verifies your own handler only; it is not a platform delivery replay or a reason to resend a real payment event manually. ## Expected result The receiver safely acknowledges durable work, ignores identical duplicates, detects conflicting IDs and handles stale notifications without granting incorrect access. ## Common mistakes - Using the complete signature as the dedupe key; it changes on retry. - Rejecting a delayed event because created_at is old even though its attempt signature is fresh. - Assuming a 302 redirect or Retry-After controls delivery. - Treating a successful HTTP response as proof that a customer can use a license. ## Security notes Pin [organization](/docs/v1/start/glossary#organization) context through endpoint configuration and environment through authenticated body checks. Protect inbox data from public downloads. Signature verification authenticates a notification, not a browser caller, a payment receipt or an application entitlement. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Attempt count does not increase | Disabled, already delivered, exhausted, locked or not yet due | Inspect status, count and next attempt before retrying | | Old event has a new signature | Each network attempt is signed again | Dedupe by body.id and verify the current timestamp | | Endpoint returns 200 but work is missing | Handler acknowledged before commit | Persist inbox/work atomically before acknowledgement | | Events arrive in reverse order | Independent failures and retries | Reconcile current state and use business-operation deduplication | ## Next steps Run the [seven verification programs](/docs/v1/guides/verify-webhook-signatures), then follow [local testing and troubleshooting](/docs/v1/guides/test-webhooks-locally). ## Related - [Webhook event catalog](/docs/v1/guides/webhook-events) - [Endpoint setup and rotation](/docs/v1/guides/webhooks-overview) - [Idempotency](/docs/v1/api/idempotency) # Verify webhook signatures Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures --- title: "Verify webhook signatures" description: "Execute raw-byte HMAC verifiers in Node, Python, PHP, C#, Go, Java and Ruby with positive and negative notification tests." slug: "/docs/v1/guides/verify-webhook-signatures" section: "Webhooks and integrations" order: 68 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/webhook-delivery","/docs/v1/guides/test-webhooks-locally"] draft: false --- Verify the original request bytes before trusting a notification or storing work for it. ## What you will learn - Run the same complete verifier contract in seven languages. - Generate a current signed synthetic test request. - Distinguish signature verification from durable deduplication and [license](/docs/v1/start/glossary#license) authorization. ## Prerequisites - Your endpoint's signing secret from [setup](/docs/v1/guides/webhooks-overview). - The [source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - A language runtime from the requirements below. - A private process environment; do not publish its secret values. The [download manifest](/docs/webhook-examples.json) lists the archive SHA-256 and every included source-file hash. Generated private requests, compiled programs and local inbox databases are not included. ## Prepare the files 1. Extract the archive and work inside its [webhooks](/docs/v1/start/glossary#webhook) directory. 2. Keep contract.json beside the verifier files. 3. Configure KDP_WEBHOOK_SECRET from your secret store with a [Sandbox](/docs/v1/start/glossary#sandbox) test endpoint secret. 4. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 5. Generate a new request directory with the following command. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. The program creates body.json and headers.json without printing the secret or signature. It refuses to overwrite existing request files; use a new directory for another test. This complete test body is a real captured synthetic local notification. It is old by design; make-test.py signs its exact bytes with the current attempt timestamp. Checking its created_at against the current clock would reject a valid delayed event incorrectly. ```json { "id": "evt_ffa11b785e0ede73d9e69801", "data": { "message": "Signed webhook test", "webhook_id": "whk_2bf9c0559190a2101b3b9d1b" }, "event": "test.ping", "created_at": "2026-10-14T17:46:36+00:00", "environment": "sandbox" } ``` ## Shared receiver contract The archive includes contract.json beside the sources. Keep it unchanged with the [local test helper](/docs/v1/guides/test-webhooks-locally#generate-and-replay-a-request); all seven programs read the same file. The event list is checked against the real sender. The 300-second freshness window and 1048576-byte body limit are explicit receiver example policy, not sender inbound limits. Header-file input is capped at 32768 bytes. The verifier rejects a missing or malformed timestamp/signature pair, wrong HMAC, stale/future attempt timestamp, unknown event, header/body mismatch and wrong pinned environment. Use the plaintext endpoint secret as the HMAC key, not its hash and not an Ed25519 public key. Verification is over timestamp, one period, and raw body bytes. No JSON canonicalization step belongs in HMAC verification. ## Requirements and commands | Language | Requirements | Run after request generation | | --- | --- | --- | | Node.js | Node 22+, standard library | node verify.mjs private/request-1/body.json private/request-1/headers.json | | Python | Python 3.11+, standard library | python3 verify.py private/request-1/body.json private/request-1/headers.json | | PHP | PHP 8.2+, JSON/hash | php verify.php private/request-1/body.json private/request-1/headers.json | | Ruby | JSON/OpenSSL, compatible syntax from 2.6; deploy maintained Ruby | ruby verify.rb private/request-1/body.json private/request-1/headers.json | | Go | Go 1.24+, standard library | Build verify.go, then run verify-go with both file paths | | Java | Java 21, Gson 2.14.0 | Compile, then run VerifyWebhook with both file paths | | C# | .NET 7 target, standard library | Build the project, then run its DLL with both file paths | Go build: ```bash go build -o verify-go verify.go ./verify-go private/request-1/body.json private/request-1/headers.json ``` Java build and run, after downloading the exact Gson JAR from the [official artifact repository](https://repo1.maven.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar): ```bash javac -cp gson-2.14.0.jar VerifyWebhook.java java -cp .:gson-2.14.0.jar VerifyWebhook private/request-1/body.json private/request-1/headers.json ``` The tested JAR SHA-256 is 2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f. On Windows use a semicolon in the Java classpath: -cp ".;gson-2.14.0.jar", and run verify-go.exe instead of ./verify-go. Use the same working directory containing contract.json for Go, Java and C#. ```bash dotnet build csharp/WebhookVerifier.csproj -c Release dotnet csharp/bin/Release/net7.0/WebhookVerifier.dll private/request-1/body.json private/request-1/headers.json ``` The .NET target matches the current SDK baseline; verify an upgrade to your supported deployment runtime before launch. This phase executes the verifiers on macOS, not native Windows or Linux. Ruby was executed on the legacy macOS runtime. Prefer maintained Ruby with the native fixed_length_secure_compare primitive for deployment; the older fallback compares every fixed-size byte position without early exit but does not establish a managed-VM constant-time guarantee. ## Complete verifier sources Choose a language to read its complete executed source and setup. Separate pages keep the initial download small without truncating or hiding code. All are public documentation and work without JavaScript. :::cards - [Node.js verifier](/docs/v1/guides/verify-webhook-signatures/node) - [Python verifier](/docs/v1/guides/verify-webhook-signatures/python) - [PHP verifier](/docs/v1/guides/verify-webhook-signatures/php) - [C# verifier](/docs/v1/guides/verify-webhook-signatures/csharp) - [Go verifier](/docs/v1/guides/verify-webhook-signatures/go) - [Java verifier](/docs/v1/guides/verify-webhook-signatures/java) - [Ruby verifier](/docs/v1/guides/verify-webhook-signatures/ruby) ::: ## HTTP adapter checklist 1. Require POST on a dedicated route and application/json content. 2. Reject duplicate signature, event-name and event-ID HTTP headers before joining header values. 3. Enforce body/header limits and a read timeout before buffering. 4. Pass exact raw body bytes and normalized lowercase header names to the verifier. 5. Pass only the secret and expected environment assigned to that endpoint. 6. On success, atomically record the event ID and durable work item. 7. Return a generic refusal on failure without echoing secrets, signatures or payloads. Expected result: an authenticated duplicate is handled by your durable inbox, not by calling the verifier alone. The CLI tools authenticate but deliberately do not perform any business effect or store a deduplication record. The [local HTTP inbox](/docs/v1/guides/test-webhooks-locally) shows the receive/commit/acknowledge boundary. ## Expected result For test-ping.json, each program exits 0 and prints JSON containing verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Object property ordering is not part of the result contract. A changed body byte, wrong secret, stale/future attempt, invalid envelope or wrong environment prints exactly {"verified":false} and exits 1. Test every failure before connecting a business action. Regenerate the request when its signature timestamp is outside the 300-second window. ## Common mistakes - Signing a pretty-printed reconstruction instead of preserving the request bytes. - Passing the webhook secret hash instead of the original secret. - Reusing a saved signature indefinitely instead of generating a new test timestamp. - Assuming the CLI verifier prevents duplicate emails or license renewals. ## Security notes The header JSON file is a local testing interface, not a public endpoint. Securely remove private generated requests under your retention policy; do not check them into Git. Reject duplicated HTTP signature headers at the adapter. Application validity uses Ed25519-signed license responses, not these HMAC notification verifiers. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false in all languages | Wrong secret, stale signature or changed body | Regenerate a private request and use its exact files | | Java cannot find Gson | JAR absent from compile/run classpath | Install the pinned artifact and use the OS-specific separator | | Go or C# cannot read contract.json | Wrong working directory | Run from the extracted webhooks directory | | A duplicate verifies twice | Authentication is not deduplication | Add a durable unique event-ID inbox before business processing | ## Next steps Run [local replay and failure tests](/docs/v1/guides/test-webhooks-locally) before enabling your production notification workflow. ## Related - [Delivery contract](/docs/v1/guides/webhook-delivery) - [Event catalog](/docs/v1/guides/webhook-events) - [Ed25519 response verification](/docs/v1/security/verify-ed25519) # Test webhooks locally Canonical: https://licensing.kdpika.com/docs/v1/guides/test-webhooks-locally --- title: "Test webhooks locally" description: "Run a private local notification inbox, test signed replay and duplicate protection, then inspect real staging deliveries." slug: "/docs/v1/guides/test-webhooks-locally" section: "Webhooks and integrations" order: 69 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/webhook-delivery","/docs/v1/guides/verify-webhook-signatures"] draft: false --- Test authentication and durable receipt without publishing your payloads to an external request-catcher service. ## What you will learn - Run a loopback-only [Sandbox](/docs/v1/start/glossary#sandbox) inbox and replay a synthetic notification. - Confirm harmless duplicate handling and refusal of changed requests. - Move from local fixtures to an HTTPS staging endpoint and inspect deliveries. ## Prerequisites - Python 3.11 or newer with standard-library SQLite. - The [webhook source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - [Webhook signature basics](/docs/v1/guides/verify-webhook-signatures). - A private working directory outside any web document root. ## Start the local inbox 1. Work inside the archive's webhooks directory. 2. Set KDP_WEBHOOK_SECRET through your private process environment to a synthetic test value or a Sandbox endpoint secret. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Set KDP_WEBHOOK_INBOX to a private local SQLite file path outside your web root. 5. Run the receiver. ```bash python3 capture.py ``` Expected output: Listening on http://127.0.0.1:8108/kdpika; Sandbox notifications only. The process binds loopback, not all network interfaces. It creates its inbox database with owner-only file permissions where the OS supports them. ```python #!/usr/bin/env python3 """Local notification inbox; never a license-validity or payment authority. Python 3.11+, standard library, verify.py and contract.json in this directory. Set KDP_WEBHOOK_SECRET, KDP_WEBHOOK_ENVIRONMENT=sandbox and KDP_WEBHOOK_INBOX. Run: python3 capture.py; POST /kdpika on 127.0.0.1:8108. Expected: accepted JSON. """ import hashlib from http.server import BaseHTTPRequestHandler, HTTPServer import json import os from pathlib import Path import sqlite3 import time from verify import verify class Receiver(BaseHTTPRequestHandler): def log_message(self, template, *args): # Do not log the raw payload, signature, full URL or secrets. return def reply(self, status, value): body = json.dumps(value, separators=(',', ':')).encode() self.send_response(status) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(body))) self.send_header('Cache-Control', 'no-store') self.send_header('X-Content-Type-Options', 'nosniff') self.end_headers() self.wfile.write(body) def do_POST(self): self.connection.settimeout(5) if self.path != '/kdpika': return self.reply(404, {'accepted': False}) lengths = self.headers.get_all('Content-Length', []) if self.headers.get('Transfer-Encoding') or len(lengths) != 1 or not lengths[0].isdigit(): return self.reply(400, {'accepted': False}) length = int(lengths[0]) if length > 1048576: return self.reply(413, {'accepted': False}) if self.headers.get('Content-Type', '').split(';')[0].strip().lower() != 'application/json': return self.reply(415, {'accepted': False}) names = ('x-lmp-signature', 'x-lmp-event-id', 'x-lmp-event') if any(len(self.headers.get_all(name, [])) != 1 for name in names): return self.reply(401, {'accepted': False}) try: body = self.rfile.read(length) if len(body) != length: return self.reply(400, {'accepted': False}) event = verify(body, {key: self.headers[key] for key in names}, self.server.secret, 'sandbox', int(time.time())) except (ValueError, TypeError, KeyError, OSError): return self.reply(401, {'accepted': False}) # A duplicate ID is acknowledged only when its authenticated body is identical. # The transaction commits before acknowledgement. No API/mail side effects. try: with sqlite3.connect(self.server.inbox, timeout=3) as database: database.execute('BEGIN IMMEDIATE') previous = database.execute('SELECT body_hash FROM inbox WHERE event_id=?', (event['id'],)).fetchone() body_hash = hashlib.sha256(body).hexdigest() if previous and previous[0] != body_hash: return self.reply(409, {'accepted': False}) database.execute('INSERT OR IGNORE INTO inbox(event_id,event,body_hash,received_at) VALUES(?,?,?,?)', (event['id'], event['event'], body_hash, int(time.time()))) database.commit() self.reply(200, {'accepted': True, 'duplicate': previous is not None}) except sqlite3.Error: self.reply(503, {'accepted': False}) def main(): secret = os.environ.get('KDP_WEBHOOK_SECRET', '') if not secret or os.environ.get('KDP_WEBHOOK_ENVIRONMENT', '').lower() != 'sandbox': raise SystemExit('Set the Sandbox endpoint secret and environment first.') configured = os.environ.get('KDP_WEBHOOK_INBOX', '') if not configured: raise SystemExit('Set KDP_WEBHOOK_INBOX to a private local file outside your web root.') inbox = Path(configured).resolve() os.umask(0o077) inbox.parent.mkdir(mode=0o700, parents=True, exist_ok=True) with sqlite3.connect(inbox) as database: database.execute('CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,event TEXT NOT NULL,body_hash TEXT NOT NULL,received_at INTEGER NOT NULL)') os.chmod(inbox, 0o600) with HTTPServer(('127.0.0.1', 8108), Receiver) as server: server.secret = secret server.inbox = str(inbox) print('Listening on http://127.0.0.1:8108/kdpika; Sandbox notifications only.', flush=True) server.serve_forever() if __name__ == '__main__': main() ``` This program is a local QA inbox, not a production HTTP server or payment fulfiller. It verifies before inserting and stores only event ID/name, body hash and receipt time. It does not save the raw [customer](/docs/v1/start/glossary#customer) payload, call a licensing API, email a key or decide [license](/docs/v1/start/glossary#license) validity. Its transaction commits before a successful acknowledgement. It intentionally has no public inbox-inspection route. ## Generate and replay a request 1. Open a second terminal with the same private environment values. 2. Generate a freshly signed synthetic request. 3. Replay it to the local receiver. ```bash python3 make-test.py events/test-ping.json private/request-1 python3 replay.py private/request-1/body.json private/request-1/headers.json ``` Expected result: the first replay returns status=200, accepted=true and duplicate=false. The signature covers the saved raw bytes. The replay tool can send only to the loopback inbox and prints no request signature or secret. The generator and verifiers share this file. Event names come from the sender; the size/freshness settings are local receiver policy. ```json { "events": [ "license.created", "license.activated", "license.deactivated", "license.suspended", "license.restored", "license.revoked", "license.renewed", "license.expiring", "license.expired", "device.reset", "test.ping" ], "environments": ["sandbox", "live"], "body_limit": 1048576, "tolerance_seconds": 300 } ``` ```python #!/usr/bin/env python3 """Sign a captured synthetic Sandbox envelope with the configured TEST secret. Python 3.11+, standard library. Run: python3 make-test.py events/test-ping.json private/request Writes body.json/headers.json to the private output directory; never sends a request. Expected: Created private test request files. No signature or secret is printed. """ import hashlib import hmac import json import os from pathlib import Path import sys import time def main(): if len(sys.argv) != 3: raise SystemExit('Supply a captured JSON file and a private output directory.') secret = os.environ.get('KDP_WEBHOOK_SECRET', '') if not secret or os.environ.get('KDP_WEBHOOK_ENVIRONMENT', '').lower() != 'sandbox': raise SystemExit('Configure a Sandbox test secret first.') body = Path(sys.argv[1]).read_bytes() event = json.loads(body) if event.get('environment') != 'sandbox': raise SystemExit('Only Sandbox test envelopes are accepted.') stamp = str(int(time.time())) headers = {'x-lmp-signature': 't=' + stamp + ',v1=' + hmac.new(secret.encode(), stamp.encode() + b'.' + body, hashlib.sha256).hexdigest(), 'x-lmp-event-id': event['id'], 'x-lmp-event': event['event']} os.umask(0o077) directory = Path(sys.argv[2]) directory.mkdir(parents=True, mode=0o700, exist_ok=True) for name, content in [('body.json', body), ('headers.json', json.dumps(headers).encode())]: path = directory / name if path.exists(): raise SystemExit('Request file already exists; use a new private directory.') with path.open('xb') as stream: stream.write(content) path.chmod(0o600) print('Created private test request files.') if __name__ == '__main__': main() ``` ```python #!/usr/bin/env python3 """Replay a private signed test request to the loopback capture.py inbox only. Python 3.11+, standard library. Run: python3 replay.py private/request-1/body.json private/request-1/headers.json Add --tamper to append a body byte. Expected: accepted JSON/exit 0, or refusal/exit 1. This tool cannot send to a public URL and does not print request secrets. """ import argparse from http.client import HTTPConnection import json from pathlib import Path def main(): parser = argparse.ArgumentParser() parser.add_argument('body') parser.add_argument('headers') parser.add_argument('--tamper', action='store_true') args = parser.parse_args() body = Path(args.body).read_bytes() headers = json.loads(Path(args.headers).read_text()) if args.tamper: body += b' ' headers['Content-Type'] = 'application/json' connection = HTTPConnection('127.0.0.1', 8108, timeout=5) try: connection.request('POST', '/kdpika', body=body, headers=headers) response = connection.getresponse() content = json.loads(response.read(4096)) print(json.dumps({'status': response.status, **content})) return 0 if 200 <= response.status < 300 else 1 except (OSError, ValueError): print('{"accepted":false,"error":"Local receiver unavailable"}') return 1 finally: connection.close() if __name__ == '__main__': raise SystemExit(main()) ``` ## Check duplicates and failure paths 1. Replay the same request again within the freshness window. 2. Confirm the result has status=200, accepted=true and duplicate=true. 3. Send a changed byte with the original signature. ```bash python3 replay.py private/request-1/body.json private/request-1/headers.json --tamper ``` Expected result: status=401, accepted=false and exit 1. No additional inbox row is accepted. A correctly re-signed different body with an existing event ID returns 409 rather than applying a second meaning to that ID. Oversized bodies return 413; unsupported content type returns 415. If durable SQLite storage is temporarily unavailable, the inbox returns 503 instead of claiming successful receipt. 4. Generate another private request after changing only the local test secret and confirm it is refused by the receiver still using the old test value. 5. Restore matching local configuration before the next positive test. 6. Restart the receiver with the same inbox file and replay an accepted ID with a fresh valid timestamp. Expected result: deduplication survives restart. Freshness checks apply to attempt timestamps; a new signature is needed after the 300-second window. There is no in-memory-only success claim. ## Inspect safely Use a local SQLite viewer to read event_id, event, body_hash and received_at from the private inbox table. The body hash detects a conflicting authenticated duplicate without retaining its full body. Do not expose that file, generated headers or request directories through a web server, Git repository or public support attachment. For real platform [delivery](/docs/v1/start/glossary#delivery), open Webhooks and match the authenticated event ID to Delivery history. The page shows the latest delivery row state and attempt count, not every historic request/response body. The [delivery reference](/docs/v1/guides/webhook-delivery#delivery-history-and-failure-states) describes retained data and the six-attempt limit. ## Use HTTPS staging or an optional tunnel The platform refuses loopback/private targets; entering http://127.0.0.1:8108/kdpika in the dashboard is not a platform-delivery test. 1. Prefer a receiver on your own HTTPS staging host with valid TLS and raw-body forwarding. 2. If local networking requires a tunnel, choose and operate it explicitly; a tunnel is an optional testing transport, never a runtime dependency for licensing. 3. Forward only the dedicated notification route to the local inbox, preserving body bytes and all three signature/event headers. 4. Verify that the public HTTPS URL resolves only to public addresses and does not redirect. 5. Create a separate Sandbox endpoint at that final URL. 6. Configure its one-time secret on the receiver. 7. Select Send test and match the event ID at both ends. 8. Stop public forwarding when testing ends and disable that temporary endpoint. Expected result: a real signed test delivery receives a 2xx acknowledgement without exposing an administration page, private inbox or general development server. A tunnel operator can carry your test bytes; do not send real customer payloads through an unreviewed service. No particular tunnel account, external catcher or fixed IP range is required by the licensing platform. ## Expected result You have reproducible evidence that valid recent requests are durably accepted, identical duplicates are harmless, altered requests fail and delivery-history status matches the receiver acknowledgement. ## Common mistakes - Disabling signature validation to make a local test pass. - Exposing the entire development server instead of one notification route. - Resetting the inbox before testing replay after restart. - Calling a license renewal during a notification-only capture test. ## Security notes Use synthetic Sandbox data. Keep signatures and keys out of screenshots and ticket text. The local receiver has no production authentication-management or operations UI; put a reviewed production HTTP adapter in front of a durable worker for real deployment. Never turn a locally re-signed fixture into a payment confirmation. ## Troubleshooting Use the dedicated [webhook troubleshooting guide](/docs/v1/guides/webhook-troubleshooting) for receiver, routing, timing and retry symptoms. | Symptom | Cause | Fix | | --- | --- | --- | | Connection refused | Local receiver stopped or port unavailable | Start capture.py and check the loopback port | | Exit 1 with 401 | Changed bytes, secret, environment or stale timestamp | Regenerate the private request with matching configuration | | Duplicate=true on the first visible replay | The durable inbox already contains that captured ID | Inspect the existing record; do not erase evidence to manufacture a first receipt | ## Next steps Review [delivery and acknowledgement rules](/docs/v1/guides/webhook-delivery), then test your real backend with a separate Sandbox endpoint. ## Related - [All notification events](/docs/v1/guides/webhook-events) - [Seven signature verifier programs](/docs/v1/guides/verify-webhook-signatures) - [Webhook troubleshooting](/docs/v1/guides/webhook-troubleshooting) # Sell with Stripe Canonical: https://licensing.kdpika.com/docs/v1/guides/stripe --- title: "Sell with Stripe" description: "Verified Stripe purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/stripe" section: "Webhooks and integrations" order: 70 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own Stripe store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Create your own Stripe Test-mode product and Price. Use a single eligible Price and quantity one in Checkout or a Payment Link. 2. For one-time sales use Checkout payment mode. For subscriptions use subscription mode. A Payment Link ultimately creates a Checkout Session and uses the same verification below. 3. In Workbench create a **Snapshot** [webhook](/docs/v1/start/glossary#webhook) destination for **Your account**, not Connect accounts. Pin its API version to 2025-07-30.basil, matching this source and Stripe-Version header. 4. Subscribe to the events in the table below; set the destination to your own HTTPS /provider-event endpoint. Save that destination's signing secret privately. 5. Set KDP_STORE_API_KEY to your private sk_test_ key, KDP_STORE_WEBHOOK_SECRET to the endpoint secret, seller_id to the acct_ ID returned by GET /v1/account, and api_version to 2025-07-30.basil. 6. Map each verified price_ ID in offer_map. Do not use a display name or a browser-selected plan as a mapping key. ## Verify before granting access Stripe-Signature authenticates the exact timestamp, one period and raw body with HMAC-SHA256. The reference accepts multiple v1 signatures, checks a 300-second timestamp window and fetches the event again with your private key. GET /v1/account must match seller_id; Connect events are refused. Event and payment objects must match the configured mode. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | checkout.session.completed / checkout.session.async_payment_succeeded | Current payment-mode Session complete, payment_status paid, one mapped line and current successful PaymentIntent/charge | Issue once; the Session ID is the create Idempotency-Key. Subscription-mode Sessions are skipped. | | invoice.paid | Current paid invoice with no remaining amount, active subscription, matching single price, initial or cycle billing reason | Initial subscription: issue with the Stripe event ID as create Idempotency-Key. Later invoice: renew once by invoice ID; restore first if suspended. | | invoice.payment_failed | Current subscription is past_due or unpaid | Suspend. An old failure received after recovery does not suspend an active subscription. | | [customer](/docs/v1/start/glossary#customer).subscription.updated / customer.subscription.deleted | Current cancellation intent or canceled state | Record cancellation; preserve already-paid license time. | | charge.refunded | Current charge fully refunded | Revoke the mapped license. Partial refunds require review. | | charge.dispute.created / charge.dispute.updated / charge.dispute.closed | Current dispute and its charge | Suspend while disputed/lost; restore on won only if not refunded. Terminal revocation cannot be undone. | Basil invoices use parent.subscription_details.subscription and line.pricing.price_details.price. Invoice payment references are read from /v1/invoice_payments; old invoice.subscription/payment_intent examples are not substituted. A Checkout success page never authorizes issuance. Delayed payments wait for confirmed paid state. Do not grant twice from checkout and invoice events. Proration/manual invoices, multiple lines and in-place plan changes are held, not guessed. Paid-out-of-band invoices can be paid without a card charge: decide whether you authorize that Stripe administrative payment state in your own operations. This sample follows authoritative invoice paid state, not a promise of bank settlement or an exact provider-period-end setter. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) - [Complete Python ingress](/docs/v1/guides/store-service/python) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php get('/v1/account'); storeRequire(($account['id'] ?? null) === $this->config['seller_id'] && empty($event['account']), 'WRONG_MERCHANT_OR_CONNECT_EVENT'); $verified = $this->get('/v1/events/' . $this->ref($event['id'] ?? null)); storeRequire(($verified['id'] ?? null) === $event['id'] && ($verified['type'] ?? null) === ($event['type'] ?? null), 'EVENT_NOT_CONFIRMED'); $this->mode($verified['livemode'] ?? null); $type = $verified['type']; $object = $verified['data']['object'] ?? []; $id = $object['id'] ?? null; if (in_array($type, ['checkout.session.completed', 'checkout.session.async_payment_succeeded'], true)) { $session = $this->get('/v1/checkout/sessions/' . $this->ref($id)); $this->mode($session['livemode'] ?? null); if (($session['mode'] ?? '') === 'subscription') { return []; } // Invoice is the sole subscription payment authority. storeRequire(($session['mode'] ?? null) === 'payment' && ($session['status'] ?? null) === 'complete', 'CHECKOUT_NOT_COMPLETE'); if (($session['payment_status'] ?? '') !== 'paid') { return []; } // Wait for delayed-payment success. $lines = $this->get('/v1/checkout/sessions/' . $this->ref($id) . '/line_items?limit=2'); storeRequire(($lines['has_more'] ?? true) === false, 'MULTI_LINE_ORDER_REQUIRES_REVIEW'); $line = $this->one($lines['data'] ?? []); $pi = $session['payment_intent'] ?? null; storeRequire(is_string($pi), 'PAYMENT_INTENT_REQUIRED'); $intent = $this->get('/v1/payment_intents/' . $this->ref($pi)); storeRequire(($intent['status'] ?? '') === 'succeeded', 'PAYMENT_NOT_SUCCEEDED'); $charge = is_string($intent['latest_charge'] ?? null) ? $intent['latest_charge'] : null; if ($charge !== null) { $current = $this->get('/v1/charges/' . $this->ref($charge)); storeRequire(($current['paid'] ?? null) === true && ($current['amount_refunded'] ?? 0) === 0 && ($current['disputed'] ?? false) === false, 'PAYMENT_ALREADY_REFUNDED_OR_DISPUTED'); } $buyer = $session['customer_details'] ?? []; return [$this->paid((string) $id, (string) $id, (string) ($line['price']['id'] ?? ''), (string) ($buyer['email'] ?? ''), (string) ($buyer['name'] ?? ''), false, array_values(array_filter([$pi, $charge])), (string) $id)]; } if (in_array($type, ['invoice.paid', 'invoice.payment_failed'], true)) { $invoice = $this->get('/v1/invoices/' . $this->ref($id)); $this->mode($invoice['livemode'] ?? null); $subId = $invoice['parent']['subscription_details']['subscription'] ?? null; storeRequire(is_string($subId), 'SUBSCRIPTION_INVOICE_REQUIRED'); $subscription = $this->get('/v1/subscriptions/' . $this->ref($subId)); $this->mode($subscription['livemode'] ?? null); if ($type === 'invoice.payment_failed') { if (!in_array($subscription['status'] ?? '', ['past_due', 'unpaid'], true)) { return []; } return [$this->negative($subId, (string) $id . ':failed', 'failed')]; } storeRequire(($invoice['status'] ?? '') === 'paid' && ($invoice['amount_remaining'] ?? -1) === 0 && ($subscription['status'] ?? '') === 'active', 'INVOICE_NOT_CURRENTLY_PAID'); storeRequire(in_array($invoice['billing_reason'] ?? '', ['subscription_create', 'subscription_cycle'], true), 'PRORATION_OR_MANUAL_INVOICE_REQUIRES_REVIEW'); storeRequire(($invoice['lines']['has_more'] ?? true) === false, 'PAGINATED_INVOICE_REQUIRES_REVIEW'); $line = $this->one($invoice['lines']['data'] ?? []); $subLine = $this->one($subscription['items']['data'] ?? []); $price = $line['pricing']['price_details']['price'] ?? null; storeRequire(is_string($price) && $price === ($subLine['price']['id'] ?? null), 'SUBSCRIPTION_PRICE_MISMATCH'); $links = [$subId, (string) $id]; $payments = $this->get('/v1/invoice_payments?invoice=' . $this->ref($id) . '&limit=2'); storeRequire(($payments['has_more'] ?? true) === false, 'PAYMENT_MAPPING_REQUIRES_REVIEW'); foreach ($payments['data'] ?? [] as $payment) { $pi = $payment['payment']['payment_intent'] ?? null; if (is_string($pi)) { $intent = $this->get('/v1/payment_intents/' . $this->ref($pi)); storeRequire(($intent['status'] ?? '') === 'succeeded', 'PAYMENT_NOT_SUCCEEDED'); $links[] = $pi; if (is_string($intent['latest_charge'] ?? null)) { $charge = $this->get('/v1/charges/' . $this->ref($intent['latest_charge'])); storeRequire(($charge['paid'] ?? null) === true && ($charge['amount_refunded'] ?? 0) === 0 && ($charge['disputed'] ?? false) === false, 'PAYMENT_ALREADY_REFUNDED_OR_DISPUTED'); $links[] = $intent['latest_charge']; } } } return [$this->paid($subId, (string) $id, $price, (string) ($invoice['customer_email'] ?? ''), (string) ($invoice['customer_name'] ?? ''), true, $links, (string) $event['id'], $invoice['billing_reason'] === 'subscription_create')]; } if ($type === 'customer.subscription.deleted' || $type === 'customer.subscription.updated') { $subscription = $this->get('/v1/subscriptions/' . $this->ref($id)); $this->mode($subscription['livemode'] ?? null); if (($subscription['cancel_at_period_end'] ?? false) || ($subscription['status'] ?? '') === 'canceled') { return [$this->negative((string) $id, (string) $event['id'], 'cancelled')]; } return []; } if ($type === 'charge.refunded') { $charge = $this->get('/v1/charges/' . $this->ref($id)); $this->mode($charge['livemode'] ?? null); storeRequire(($charge['refunded'] ?? false) && ($charge['amount_refunded'] ?? -1) === ($charge['amount'] ?? -2), 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) ($charge['payment_intent'] ?? ''), (string) $event['id'], 'refunded')]; } if (str_starts_with($type, 'charge.dispute.')) { $dispute = $this->get('/v1/disputes/' . $this->ref($id)); $this->mode($dispute['livemode'] ?? null); $chargeId = $dispute['charge'] ?? null; $charge = $this->get('/v1/charges/' . $this->ref($chargeId)); $state = ($dispute['status'] ?? '') === 'won' ? 'recovered' : 'disputed'; storeRequire(($charge['amount_refunded'] ?? 0) === 0 || $state !== 'recovered', 'REFUNDED_DISPUTE_REQUIRES_REVIEW'); return [$this->negative((string) $chargeId, (string) $event['id'], $state)]; } return []; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real Stripe account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before [Live](/docs/v1/start/glossary#live) use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://docs.stripe.com/webhooks) - [Official reference 2](https://docs.stripe.com/checkout/fulfillment) - [Official reference 3](https://docs.stripe.com/billing/subscriptions/webhooks) - [Official reference 4](https://docs.stripe.com/api/invoices/object?api-version=2025-07-30.basil) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Sell with Paddle Billing Canonical: https://licensing.kdpika.com/docs/v1/guides/paddle --- title: "Sell with Paddle Billing" description: "Verified Paddle Billing purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/paddle" section: "Webhooks and integrations" order: 71 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own Paddle Billing store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Use a Paddle Billing Sandbox account, not Paddle Classic. Create a one-time or recurring price and save its pri_ ID. 2. Create a notification destination pointing to your own HTTPS /provider-event endpoint. Select transaction.completed, transaction.payment_failed, subscription.updated, subscription.canceled, subscription.past_due, subscription.paused, adjustment.created and adjustment.updated. 3. Save that destination's secret as KDP_STORE_WEBHOOK_SECRET and a private Sandbox API key as KDP_STORE_API_KEY. Its permissions must allow reading transactions, [customers](/docs/v1/start/glossary#customer), subscriptions and adjustments. 4. Set provider/source to paddle, environment to sandbox and seller_id to your own stable nonpersonal merchant reference. The API key and destination must belong to that same account; do not accept a merchant ID supplied by a buyer. 5. Map each pri_ ID in offer_map. Confirm it is the single quantity-one price in your checkout. ## Verify before granting access Paddle-Signature uses ts, a colon and the exact raw body, HMAC-SHA256 and h1 values. This reference follows a five-second clock tolerance, so accurate server time and immediate ingress verification matter. API reads use sandbox-api.paddle.com in Sandbox and api.paddle.com in [Live](/docs/v1/start/glossary#live). Account ownership is the scoped API key/destination pair, not an invented livemode field. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | transaction.completed | Current completed transaction, no approved adjustment, one price, verified customer; subscription active when present | Issue for initial api/web origin; subscription_recurring renews an existing mapping once by transaction ID. | | transaction.payment_failed | Current transaction is not completed and has an existing subscription mapping | Suspend; a recovered completed transaction is not suspended by the old failure. | | subscription.updated / subscription.canceled / subscription.past_due / subscription.paused | Read current subscription state/scheduled_change | Record cancel intent without losing paid time; suspend current past_due/paused state. | | adjustment.created / adjustment.updated | Current approved adjustment belongs to the mapped transaction | Full refund revokes; chargeback/warning suspends; confirmed reverse restores. Partial/unknown adjustments require review. | Only the initial api/web purchase may create a mapping. A renewal arriving without its initial purchase is held. Subscription changes, prorated amounts and mixed baskets are not automatically converted into another license plan. The reference queries approved adjustments before granting a transaction again; duplicate notifications do not add another renewal. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste [webhook](/docs/v1/start/glossary#webhook) endpoint. ```php get('/transactions/' . $this->ref($native['id'] ?? null))['data'] ?? []; $order = $transaction['subscription_id'] ?? $transaction['id'] ?? ''; if ($type === 'transaction.payment_failed') { if (($transaction['status'] ?? '') === 'completed') { return []; } return [$this->negative((string) $order, (string) $event['event_id'], 'failed')]; } if ($type !== 'transaction.completed') { return []; } storeRequire(($transaction['status'] ?? '') === 'completed', 'TRANSACTION_NOT_COMPLETED'); $adjustments = $this->get('/adjustments?transaction_id=' . $this->ref($transaction['id']) . '&status=approved&per_page=1'); storeRequire(($adjustments['data'] ?? null) === [], 'ADJUSTED_PAYMENT_REQUIRES_REVIEW'); $item = $this->one($transaction['items'] ?? []); $customer = $this->get('/customers/' . $this->ref($transaction['customer_id'] ?? null))['data'] ?? []; $recurring = is_string($transaction['subscription_id'] ?? null); storeRequire(in_array($transaction['origin'] ?? '', ['api', 'web', 'subscription_recurring'], true), 'PRORATION_OR_ONE_TIME_SUBSCRIPTION_CHARGE_REQUIRES_REVIEW'); if ($recurring) { $subscription = $this->get('/subscriptions/' . $this->ref($transaction['subscription_id']))['data'] ?? []; storeRequire(($subscription['status'] ?? '') === 'active', 'SUBSCRIPTION_NOT_ACTIVE'); } $initial = ($transaction['origin'] ?? '') !== 'subscription_recurring'; return [$this->paid((string) $order, (string) $transaction['id'], (string) ($item['price']['id'] ?? ''), (string) ($customer['email'] ?? ''), (string) ($customer['name'] ?? ''), $recurring, [(string) $transaction['id']], null, $initial)]; } if (str_starts_with($type, 'subscription.')) { $subscription = $this->get('/subscriptions/' . $this->ref($native['id'] ?? null))['data'] ?? []; $state = match ($subscription['status'] ?? '') { 'past_due', 'paused' => 'failed', 'canceled' => 'cancelled', default => null }; if ($state === null && ($subscription['scheduled_change']['action'] ?? '') === 'cancel') { $state = 'cancelled'; } return $state === null ? [] : [$this->negative((string) $subscription['id'], (string) $event['event_id'], $state)]; } if (str_starts_with($type, 'adjustment.')) { $adjustment = $this->get('/adjustments/' . $this->ref($native['id'] ?? null))['data'] ?? []; if (($adjustment['status'] ?? '') !== 'approved') { return []; } $kind = $adjustment['action'] ?? ''; if (in_array($kind, ['chargeback', 'chargeback_warning', 'chargeback_reverse', 'chargeback_warning_reverse'], true)) { return [$this->negative((string) ($adjustment['transaction_id'] ?? ''), (string) $event['event_id'], str_ends_with($kind, '_reverse') ? 'recovered' : 'disputed')]; } storeRequire(($adjustment['action'] ?? '') === 'refund', 'ADJUSTMENT_POLICY_REQUIRES_REVIEW'); $transaction = $this->get('/transactions/' . $this->ref($adjustment['transaction_id'] ?? null))['data'] ?? []; $total = (string) ($transaction['details']['totals']['total'] ?? ''); storeRequire($total !== '' && (string) ($adjustment['totals']['total'] ?? '') === $total, 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) $transaction['id'], (string) $event['event_id'], 'refunded')]; } return []; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real Paddle Billing account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before Live use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://developer.paddle.com/webhooks/about/signature-verification/) - [Official reference 2](https://developer.paddle.com/webhooks/transactions/transaction-completed/) - [Official reference 3](https://developer.paddle.com/api-reference/adjustments/list-adjustments/) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Sell with Lemon Squeezy Canonical: https://licensing.kdpika.com/docs/v1/guides/lemon-squeezy --- title: "Sell with Lemon Squeezy" description: "Verified Lemon Squeezy purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/lemon-squeezy" section: "Webhooks and integrations" order: 72 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own Lemon Squeezy store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Create a Test-mode store product and variant. Decide whether it is one-time or subscription; save the variant and store IDs. 2. Create a signed [webhook](/docs/v1/start/glossary#webhook) for your own HTTPS /provider-event endpoint. Select order_created, order_refunded, subscription_payment_success, subscription_payment_failed, subscription_payment_recovered, subscription_payment_refunded, subscription_cancelled, subscription_updated and subscription_expired. 3. Set KDP_STORE_WEBHOOK_SECRET to your chosen webhook secret and KDP_STORE_API_KEY to your private API key. 4. Set provider/source to lemon-squeezy, environment to sandbox and seller_id to the numeric store ID as a JSON string. 5. Map variant IDs as strings in offer_map. The reference uses JSON:API headers, verifies store_id and test_mode and reads current order/subscription/invoice records. ## Verify before granting access X-Signature is the hexadecimal HMAC-SHA256 of raw request bytes. It does not provide a timestamp; durable business-effect deduplication is essential. meta.event_name identifies the notification type but does not replace authenticated current API reads. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | order_created | Current paid one-time order; authenticated order-items query confirms exactly one quantity-one line; variant is not a subscription | Issue once by order ID. Subscription orders are skipped so the invoice is the sole initial payment authority. | | subscription_payment_success / subscription_payment_recovered | Current paid, nonrefunded invoice; active subscription; billing_reason initial or renewal | Issue initial invoice; renew an existing subscription once per invoice. Recovering a suspended license restores, then renews once. | | subscription_payment_failed / subscription_updated | Current subscription state is past_due | Suspend; stale failure after current recovery does not remove active access. | | subscription_cancelled / subscription_expired | Current cancellation/expiry state | Cancel intent preserves paid access; expired suspends. | | order_refunded / subscription_payment_refunded | Current order/invoice confirms a full refund | Revoke mapped access. Partial refunds stop for review. | The first_order_item field alone does not prove a single-item purchase: the reference additionally queries order-items filtered by the authenticated order ID. Subscription invoice billing_reason updated is not treated as a normal new paid period. Lemon Squeezy native [license keys](/docs/v1/start/glossary#license-key) are a different system; these examples create KDPika licenses only. A generic dispute webhook is not invented: reconcile chargeback outcomes through supported merchant records and your reviewed policy. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php get('/v1/' . $resource . '/' . $this->ref($native['id'] ?? null))['data'] ?? []; $attributes = $current['attributes'] ?? []; $this->mode(isset($attributes['test_mode']) ? !$attributes['test_mode'] : null); if ($resource === 'orders') { storeRequire((string) ($attributes['store_id'] ?? '') === (string) $this->config['seller_id'], 'WRONG_STORE'); if ($type === 'order_refunded') { storeRequire(($attributes['refunded'] ?? false) && ($attributes['refunded_amount'] ?? -1) === ($attributes['total'] ?? -2), 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) $current['id'], hash('sha256', json_encode($current, JSON_THROW_ON_ERROR)), 'refunded')]; } if ($type !== 'order_created') { return []; } storeRequire(($attributes['status'] ?? '') === 'paid', 'ORDER_NOT_PAID'); $items = $this->get('/v1/order-items?filter%5Border_id%5D=' . $this->ref($current['id']) . '&page%5Bsize%5D=2'); storeRequire(($items['meta']['page']['total'] ?? -1) === 1, 'MULTI_LINE_ORDER_REQUIRES_REVIEW'); $item = $this->one($items['data'] ?? []); $line = $item['attributes'] ?? []; storeRequire((string) ($line['order_id'] ?? '') === (string) $current['id'] && (int) ($line['quantity'] ?? 0) === 1, 'SINGLE_UNIT_LINE_REQUIRED'); $variant = $this->get('/v1/variants/' . $this->ref($line['variant_id'] ?? null))['data']['attributes'] ?? []; storeRequire(is_bool($variant['is_subscription'] ?? null), 'VARIANT_KIND_NOT_CONFIRMED'); if (($variant['is_subscription'] ?? false) === true) { return []; } return [$this->paid((string) $current['id'], (string) $current['id'], (string) ($line['variant_id'] ?? ''), (string) ($attributes['user_email'] ?? ''), (string) ($attributes['user_name'] ?? ''), false)]; } $subscription = $resource === 'subscriptions' ? $current : ($this->get('/v1/subscriptions/' . $this->ref($attributes['subscription_id'] ?? null))['data'] ?? []); $sub = $subscription['attributes'] ?? []; storeRequire((string) ($sub['store_id'] ?? '') === (string) $this->config['seller_id'], 'WRONG_STORE'); $this->mode(isset($sub['test_mode']) ? !$sub['test_mode'] : null); $order = (string) ($subscription['id'] ?? ''); if (in_array($type, ['subscription_payment_success', 'subscription_payment_recovered'], true)) { storeRequire(($attributes['status'] ?? '') === 'paid' && ($attributes['refunded'] ?? false) === false && ($attributes['refunded_amount'] ?? 0) === 0 && ($sub['status'] ?? '') === 'active', 'SUBSCRIPTION_NOT_PAID'); storeRequire(in_array($attributes['billing_reason'] ?? '', ['initial', 'renewal'], true), 'PRORATION_REQUIRES_REVIEW'); return [$this->paid($order, (string) $current['id'], (string) ($sub['variant_id'] ?? ''), (string) ($sub['user_email'] ?? ''), (string) ($sub['user_name'] ?? ''), true, [(string) ($sub['order_id'] ?? ''), (string) $current['id']], null, $attributes['billing_reason'] === 'initial')]; } $state = match (true) { $type === 'subscription_payment_refunded' => 'refunded', ($sub['status'] ?? '') === 'past_due' => 'failed', ($sub['cancelled'] ?? false) === true || ($sub['status'] ?? '') === 'cancelled' => 'cancelled', ($sub['status'] ?? '') === 'expired' => 'ended', default => null }; if ($state === 'refunded') { storeRequire(($attributes['refunded'] ?? false) && ($attributes['refunded_amount'] ?? -1) === ($attributes['total'] ?? -2), 'PARTIAL_REFUND_REQUIRES_REVIEW'); } return $state === null ? [] : [$this->negative($order, hash('sha256', json_encode($current, JSON_THROW_ON_ERROR)), $state)]; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real Lemon Squeezy account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before [Live](/docs/v1/start/glossary#live) use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://docs.lemonsqueezy.com/help/webhooks/signing-requests) - [Official reference 2](https://docs.lemonsqueezy.com/help/webhooks/event-types) - [Official reference 3](https://docs.lemonsqueezy.com/api/order-items/list-all-order-items) - [Official reference 4](https://docs.lemonsqueezy.com/api/subscription-invoices/the-subscription-invoice-object) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Sell with Gumroad Canonical: https://licensing.kdpika.com/docs/v1/guides/gumroad --- title: "Sell with Gumroad" description: "Verified Gumroad purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/gumroad" section: "Webhooks and integrations" order: 73 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own Gumroad store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Create a product or membership in your own Gumroad account and obtain a private access token authorized to read that account's sales. 2. Configure Gumroad Ping/resource notifications for supported sale, refund, dispute, dispute_won, cancellation and subscription lifecycle notifications to your own HTTPS /provider-event endpoint. Preserve the original form-encoded body and sale_id. 3. Set provider/source to gumroad, seller_id to your merchant seller ID and KDP_STORE_API_KEY to the private access token. The reference does not require an invented [webhook](/docs/v1/start/glossary#webhook) signing secret. 4. Add gumroad_mode_policy with value configured_environment to private config.json. Start with environment sandbox and a Sandbox KDPika key. Gumroad's sale response has no universal mode flag; this is explicit environment policy, not provider-confirmed [Live](/docs/v1/start/glossary#live)/Test separation. 5. Map the authenticated sale's product_id in offer_map. Do not accept the product, email or payment claim from Ping without the API verification. ## Verify before granting access Ping is an unsigned hint, not payment proof. The reference retrieves /v2/sales/{sale_id} with the private token and checks seller_id and sale ID. Before a grant it additionally checks that sale is listed by the authenticated successful-sales API. A paid boolean alone means a nonzero price in current Gumroad source, not successful bank collection. No fake HMAC is introduced. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | Sale Ping/resource notification | Authenticated sale, matching successful-sales entry, paid nonzero price, quantity one, no partial refund | Issue once by sale ID; membership uses stable subscription_id and renews once per subsequent sale ID. | | Membership recurring charge | Same authenticated membership/[customer](/docs/v1/start/glossary#customer)/product, recurring_charge true | Renew only an existing initial purchase. A later charge cannot create a missing initial mapping. | | Cancellation / ended state | Current sale.cancelled or sale.ended from the API | Cancellation preserves paid time; ended suspends. | | Refund / dispute / dispute_won notification | Current authenticated refunded/disputed/dispute_won flags | Full refunded revokes; current dispute suspends; confirmed won restores unless terminally revoked. | An unsigned caller can cause bounded verification reads but cannot choose a recipient or mint from arbitrary fields. The global 60/minute ingress cap bounds those reads; use your own edge controls against denial of service. A Gumroad Test purchase may not appear in the successful-sales listing, so it is held rather than accepted merely because a test Ping arrived. Contract fixtures are not a claim that a Gumroad Test checkout was executed. There is no universal failed-charge proof or mode discriminator in this adapter. Missing/restricted sales, membership failure details, partial refunds and ambiguous renewal data require merchant review. Do not copy subscription_*_at fields from Gumroad's separate native license-verification API into the sales contract. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php get('/v2/sales/' . $this->ref($id)); $sale = $result['sale'] ?? []; storeRequire(($result['success'] ?? false) && ($sale['id'] ?? null) === $id && ($sale['seller_id'] ?? null) === $this->config['seller_id'], 'SALE_NOT_VERIFIED'); // Gumroad's documented sale response has no universal mode flag. // Pin the destination environment explicitly; never trust Ping's test field. storeRequire(($this->config['gumroad_mode_policy'] ?? '') === 'configured_environment', 'EXPLICIT_GUMROAD_MODE_POLICY_REQUIRED'); $order = (string) ($sale['subscription_id'] ?? $sale['id']); $state = match (true) { ($sale['refunded'] ?? false) === true => 'refunded', ($sale['disputed'] ?? false) === true && ($sale['dispute_won'] ?? false) === true => 'recovered', ($sale['disputed'] ?? false) === true && ($sale['dispute_won'] ?? false) !== true => 'disputed', ($sale['ended'] ?? false) === true => 'ended', ($sale['cancelled'] ?? false) === true => 'cancelled', default => 'paid' }; if ($state !== 'paid') { return [$this->negative($order, hash('sha256', json_encode($sale, JSON_THROW_ON_ERROR)), $state)]; } // GET sale by ID also returns non-successful purchases in current Gumroad // source. "paid" means nonzero price, NOT successful bank collection. $listed = $this->get('/v2/sales?order_id=' . $this->ref($sale['order_id'] ?? null)); $matches = array_values(array_filter($listed['sales'] ?? [], static fn(array $item): bool => ($item['id'] ?? null) === ($sale['id'] ?? null))); storeRequire(($listed['success'] ?? false) && count($matches) === 1, 'SALE_NOT_IN_SUCCESSFUL_ENTITLEMENTS'); storeRequire(($sale['paid'] ?? false) === true && (int) ($sale['quantity'] ?? 1) === 1 && ($sale['partially_refunded'] ?? false) === false, 'UNPAID_PARTIAL_OR_MULTI_SEAT_REQUIRES_REVIEW'); return [$this->paid($order, (string) $sale['id'], (string) ($sale['product_id'] ?? ''), (string) ($sale['email'] ?? ''), (string) ($sale['full_name'] ?? ''), !empty($sale['subscription_id']), [(string) $sale['id']], null, ($sale['recurring_charge'] ?? false) === false)]; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real Gumroad account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before Live use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://gumroad.com/api) - [Official reference 2](https://github.com/antiwork/gumroad/blob/main/app/controllers/api/v2/sales_controller.rb) - [Official reference 3](https://github.com/antiwork/gumroad/blob/main/app/javascript/components/ApiDocumentation/Endpoints/Sales.tsx) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # WooCommerce legacy service reference Canonical: https://licensing.kdpika.com/docs/v1/guides/woocommerce/legacy-service --- title: "WooCommerce legacy service reference" description: "Verified WooCommerce / WordPress purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/woocommerce/legacy-service" section: "Webhooks and integrations" order: 74 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- This is the legacy developer-operated service reference. New stores should use the [official installable WooCommerce plugin](/docs/v1/guides/woocommerce), without a separate service, JSON mapping or [developer](/docs/v1/start/glossary#developer) cron. Keep this reference only for integrations already using these exact source files. Do not run both issuers on the same order. ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Install supported WordPress/WooCommerce with working Action Scheduler and HTTPS. Create a dedicated REST read key authorized to read your orders; keep its consumer key/secret privately as KDP_STORE_API_KEY and KDP_STORE_API_SECRET. 2. Set provider/source to woocommerce, environment to sandbox, seller_id to a stable merchant reference, store_url to the exact HTTPS origin without a trailing slash, and woocommerce_mode_policy to configured_environment. 3. Use a payment plugin's own Test mode on that store. WooCommerce core has no universal order Test flag: pin a dedicated test store/key to KDPika Sandbox. Do not claim automatic payment-plugin mode detection. 4. Map a quantity-one variation ID, or product ID when no variation is present, in offer_map. 5. Install the complete plugin shown below as wp-content/plugins/kdpika-store-bridge/kdpika-store-bridge.php, then activate it. Set KDP_LICENSE_WEBHOOK_URL to your own service's HTTPS /provider-event and KDP_LICENSE_WEBHOOK_SECRET to the same private secret as KDP_STORE_WEBHOOK_SECRET in environment.php. 6. Place those constants in private wp-config.php server configuration, not a page editor. Complete an order and inspect WooCommerce Scheduled Actions. Run the separate service cron to fulfill it. ## Verify before granting access The plugin signs the exact JSON body with base64 HMAC-SHA256 in X-WC-Webhook-Signature. This matches the native WooCommerce raw-body signature format, but these two plugin hook notices are this reference bridge, not a claim that every WooCommerce extension emits the same body. The worker retrieves the current order through authenticated /wp-json/wc/v3/orders/{id}; browser checkout fields are not payment proof. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | woocommerce_order_status_completed | REST order completed, date_paid present, no refunds, one mapped quantity-one line | Asynchronously queue a notice, then issue once after the private worker verifies it. | | woocommerce_order_refunded | REST order is fully refunded | Revoke the mapped license; partial refunds stop for review. | | Failed/cancelled current order received by an order notice | Authenticated current state, existing mapping | Failed suspends; cancellation intent preserves paid time. The minimal plugin itself only subscribes to completed/refunded hooks. | Do not also configure a second custom issuer on the same order. This bridge retries failed receipt five times with increasing delays, then exposes a failed scheduled action for review; a 202 receipt is not a license/email confirmation. Check Action Scheduler and your service independently. This is an order-based bridge. WooCommerce Subscriptions is a separate extension with its own renewal/subscription hooks and identifiers; this minimal plugin does not implement that extension or invent universal recurring hooks. A renewal treated as an independent completed order creates an independent order license, not a renewal of the original entitlement. Native WordPress installation tests require your own staging store; the supplied test executes the exact plugin with explicit WordPress/WooCommerce contract functions. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste [webhook](/docs/v1/start/glossary#webhook) endpoint. ```php get('/wp-json/wc/v3/orders/' . $this->ref($orderId)); storeRequire((string) ($order['id'] ?? '') === (string) $orderId, 'ORDER_NOT_CONFIRMED'); // WooCommerce has no universal test flag across payment plugins. storeRequire(($this->config['woocommerce_mode_policy'] ?? '') === 'configured_environment', 'EXPLICIT_WOO_MODE_POLICY_REQUIRED'); $id = (string) $orderId; $status = $order['status'] ?? ''; if ($status === 'refunded') { return [$this->negative($id, 'refund:' . $id, 'refunded')]; } if ($status === 'cancelled') { return [$this->negative($id, 'cancel:' . $id, 'cancelled')]; } if ($status === 'failed') { return [$this->negative($id, 'failed:' . $id, 'failed')]; } storeRequire($status === 'completed' && !empty($order['date_paid']) && empty($order['refunds']), 'ORDER_NOT_COMPLETED_OR_REFUNDED'); $line = $this->one($order['line_items'] ?? []); $offer = (string) (($line['variation_id'] ?? 0) ?: ($line['product_id'] ?? '')); $buyer = $order['billing'] ?? []; return [$this->paid($id, $id, $offer, (string) ($buyer['email'] ?? ''), trim(($buyer['first_name'] ?? '') . ' ' . ($buyer['last_name'] ?? '')), false)]; } } ``` ## Complete WordPress plugin ```php $orderId, 'attempt' => 0], 'kdpika-licensing'); } add_action('woocommerce_order_status_completed', 'kdpika_queue_order_notice', 10, 1); add_action('woocommerce_order_refunded', 'kdpika_queue_order_notice', 10, 1); function kdpika_send_order_notice(int $orderId, int $attempt): void { if (!defined('KDP_LICENSE_WEBHOOK_URL') || !defined('KDP_LICENSE_WEBHOOK_SECRET')) { return; } $url = (string) KDP_LICENSE_WEBHOOK_URL; if (wp_parse_url($url, PHP_URL_SCHEME) !== 'https' || wp_parse_url($url, PHP_URL_PATH) !== '/provider-event' || strlen((string) KDP_LICENSE_WEBHOOK_SECRET) < 16) { return; } $order = wc_get_order($orderId); if (!$order) { return; } $body = wp_json_encode(['id' => $orderId, 'status' => $order->get_status()]); if (!is_string($body)) { return; } $signature = base64_encode(hash_hmac('sha256', $body, (string) KDP_LICENSE_WEBHOOK_SECRET, true)); $result = wp_remote_post($url, ['timeout' => 10, 'redirection' => 0, 'sslverify' => true, 'headers' => ['Content-Type' => 'application/json', 'X-WC-Webhook-Signature' => $signature, 'X-WC-Webhook-Topic' => 'order.updated'], 'body' => $body]); $accepted = !is_wp_error($result) && wp_remote_retrieve_response_code($result) === 202; if ($accepted) { $receipt = json_decode(wp_remote_retrieve_body($result), true); $accepted = is_array($receipt) && ($receipt['accepted'] ?? false) === true && is_bool($receipt['duplicate'] ?? null); } if (!$accepted && $attempt < 5 && function_exists('as_schedule_single_action')) { $delays = [60, 300, 1800, 7200, 43200]; as_schedule_single_action(time() + $delays[$attempt], 'kdpika_store_notice', ['order_id' => $orderId, 'attempt' => $attempt + 1], 'kdpika-licensing'); } elseif (!$accepted) { throw new RuntimeException('Licensing notification not confirmed. Review the private service and this scheduled action before retrying.'); } } add_action('kdpika_store_notice', 'kdpika_send_order_notice', 10, 2); ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real WooCommerce / WordPress account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before [Live](/docs/v1/start/glossary#live) use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://woocommerce.github.io/woocommerce-rest-api-docs/#webhooks) - [Official reference 2](https://woocommerce.github.io/woocommerce-rest-api-docs/#retrieve-an-order) - [Official reference 3](https://actionscheduler.org/usage/) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # KDPika Licensing for WooCommerce Canonical: https://licensing.kdpika.com/docs/v1/guides/woocommerce --- title: "KDPika Licensing for WooCommerce" description: "Install the official plugin, connect your workspace and deliver software licenses through your WooCommerce store." slug: "/docs/v1/guides/woocommerce" section: "Webhooks and integrations" order: 74 audience: "all" level: "beginner" last_updated: "2026-10-19" version: "v1" related: ["/docs/v1/start/quickstart", "/docs/v1/guides/woocommerce/legacy-service"] draft: false --- [Developers](/docs/v1/start/glossary#developer) build software. KDPika handles licensing. Your WooCommerce store keeps checkout, payments, orders, [customer](/docs/v1/start/glossary#customer) accounts and customer communications. Buyers do not need a KDPika account. ## What you will learn - Install a ready-to-use ZIP and connect automatically or manually. - Map simple [products](/docs/v1/start/glossary#product) and individual variations to real [license](/docs/v1/start/glossary#license) plans. - Deliver licenses, handle refunds and recover safely from interruptions. ## Prerequisites - An active KDPika workspace with a software product and active license plan. - WordPress 6.5+, PHP 8.1+ with OpenSSL and WooCommerce 9.0+ on a public HTTPS address. - Permission to manage WooCommerce and approve the connection as a KDPika workspace owner or administrator. - HTTPS requests allowed in both directions between the store and KDPika. Both pretty and plain WordPress permalinks are supported. You do not need a separate Store Integration Service, API scripts, JSON configuration, Composer, Node, SSH or manual cron setup. WooCommerce's normal background processing must be working. ## Download and install 1. Sign in to KDPika and open **WooCommerce** in the developer dashboard. 2. Choose **Download WordPress plugin** to get `kdpika-licensing-for-woocommerce.zip`. 3. In WordPress open **Plugins → Add New → Upload Plugin**. Choose the ZIP, install and activate it. 4. Open **[KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing)** in the WordPress administration menu. The plugin declares HPOS compatibility and uses WooCommerce order CRUD, not legacy order-table queries. If WooCommerce is missing, activate it first; the plugin displays a dependency notice rather than starting fulfillment. ## Automatic connection 1. Click **Connect to KDPika** in the plugin. 2. Sign in to KDPika if requested. Check the workspace, your email and the requesting WordPress address. 3. Select **[Sandbox](/docs/v1/start/glossary#sandbox)** for your first test. Select only the software products that this store needs. 4. Click **Approve and return to WordPress**. 5. Confirm the connected workspace and environment in the plugin, then map a product. The plugin exchanges a short-lived, single-use authorization code on the server using S256 PKCE. You do not copy account-wide keys. The store must prove control of its WordPress installation before a connection can be granted. ## Manual connection If automatic connection is unavailable: 1. In the plugin, open **Connect manually instead**. Copy its exact WordPress address. 2. In the KDPika WooCommerce dashboard, open **Manual connection**. 3. Enter that address, select the environment and allowed software products, then generate a one-time setup credential. 4. Paste the credential into the plugin and choose **Verify & connect** within 15 minutes. The setup credential is shown once, tied to that store address and usable once. After connection, both methods have the same scoped licensing functionality. Saved credentials are encrypted and are never displayed again. Reconnect the same installation to rotate credentials without deleting its mappings or licenses. ## Map products and variations Open **KDPika Licensing → Products** to search your WooCommerce catalog. Enable licensing, choose the software product / license plan from the fetched list, select fulfillment timing and save. Use the filter above the plan list to find a product or plan quickly. No internal IDs need to be entered. Alternatively, use the **KDPika Licensing** tab in the WooCommerce product editor. For variable products, configure each variation separately in its variation panel or the plugin product list. A parent mapping is not inherited by variations. - **When paid:** issue after WooCommerce confirms payment and the order is Processing or Completed; suitable for virtual/downloadable goods. - **When completed:** wait for Completed even if payment is already confirmed; suitable for physical fulfillment. - **Quantity:** one license per purchased unit, with a maximum of 50 licensed units per order. Fractional quantities require review. The order line saves the mapping that applied when it was created. Later product edits do not silently change old orders. Invalid/deleted software plans or mismatched mappings stop for review instead of issuing a different license. Existing historical orders without a saved mapping are not blindly backfilled. ## License issuance and delivery The store queues eligible order changes asynchronously. KDPika retrieves a fresh signed order proof from that exact store, checks its connection and mappings, and commits the license and encrypted [delivery](/docs/v1/start/glossary#delivery) reference in one database transaction. Repeated notifications, retries and concurrent workers reuse the centrally unique entitlement. In **Settings**, choose delivery through: - WooCommerce customer emails. - My Account order details for the order owner. - Guest order details protected by WooCommerce's secure order key. When background fulfillment finishes after the original order email, the store sends a native **Software licenses** WooCommerce email. It does not deliberately send a second license email if the original customer order email already included the keys. Configure that message in **WooCommerce → Settings → Emails**; its templates can be overridden by the merchant's theme in the usual WooCommerce template locations. Mail acceptance is not a guarantee of inbox placement, and interrupted mail delivery must be reviewed before any manual resend. Full keys do not appear in public API replies, frontend JavaScript or diagnostics. Store administrators can see the relevant keys in the order administration screen. KDPika does not create another buyer portal or send a competing buyer email for these store licenses. ## Subscriptions and renewals WooCommerce Subscriptions is a separate paid extension, not WooCommerce core and not included in this ZIP. When installed, the plugin uses its native subscription and order APIs. - A confirmed initial payment creates the subscription-item entitlement. - Confirmed renewals reuse that license and synchronize its exact paid-period end, rather than adding an arbitrary number of days on every [webhook](/docs/v1/start/glossary#webhook). - Payment failures can preserve a configurable **0–14-day grace period**, defaulting to three days. - A hold unrelated to a confirmed payment failure suspends the license without inventing a grace entitlement. - Pending cancellation preserves already-paid access until the recorded paid end. - Verified payment recovery can restore eligible access; a store administrator's manual suspension is not silently undone by a resynchronization. - Revoked/cancelled terminal licenses cannot be automatically restored. Paid subscription periods take precedence over the ordinary plan duration for these subscription entitlements. Ambiguous subscription-item matching, unsupported schedule states, missing paid-period evidence and changes to a different mapped software product/plan stop for administrator review. The plugin does not guess a prorated license migration or change the customer's payment subscription itself. ## Refunds and cancellations | Store state | License behavior | | --- | --- | | Unpaid/pending order | No new license | | Full order refund | Revoke the licenses belonging to that order's mapped items | | Line-item quantity refund | Revoke only the refunded units; other units/items remain unchanged | | Unrelated refunded product | Do not revoke the other mapped products | | Amount-only or ambiguous partial refund | Require review; do not revoke every license by assumption | | Failed/cancelled order with an existing one-time entitlement | Suspend recoverably rather than creating another license | Generic gateway disputes are not treated as universally available WooCommerce events. If a payment extension does not expose a reliable order/refund state, review the payment and use the authorized license controls. The plugin does not implement another payment gateway. ## Recovery and diagnostics Open **Orders** for fulfillment state, safe retry and resynchronization. Network errors retry with bounded exponential backoff and jitter. Persistent failures remain visible as **Needs attention**; a paid order's recovery record is not silently discarded. Open **Diagnostics** to test the signed connection. Check HTTPS reachability, WordPress background processing, active mappings, the order's real payment/refund state and the selected environment. If WordPress security salts changed or the store moved, reconnect that installation. Changing the browser's success URL cannot authorize a license. The plugin automatically reconciles recent order changes through WooCommerce APIs. Reconciliation checks current evidence; it never blindly issues a new key merely because a local response was lost. Use **Licenses** to search/filter masked records and perform confirmed suspend, eligible restore or permanent revoke actions. ## Security notes Each store has a separate revocable credential limited to approved software products and its own licensing operations. It cannot access other developers' stores or change platform billing. Signed requests bind method, route, timestamp, nonce and exact body. Replay checks and rate limits apply centrally and on private store endpoints. KDPika processes only necessary store/order/item/subscription references, buyer email/name needed for licensing, and confirmed fulfillment state. Your store owns the buyer relationship and must provide its own privacy notices. Bank-card data is neither collected nor stored by this plugin. Deactivate without deleting paid customer records. Uninstall removes local credentials but retains operational mappings/recovery data and encrypted order delivery records. Disconnect in KDPika to revoke server access; uninstall does not cancel customers' WooCommerce subscriptions. Keep private backups and apply your retention obligations. ## Testing status and Live checklist The initial candidate was installed from its actual ZIP on native local WordPress 7.1.3 and WooCommerce 11.2.1 with HPOS. Verified cases include real bidirectional local HTTP authorization, mapping, unpaid refusal, multi-unit issuance, duplicate/retry safety, concurrent processing, quantity refund, private order delivery and WooCommerce email rendering. The local payment state was set through WooCommerce test CRUD; no genuine payment-gateway charge or real inbox delivery is claimed. The commercial WooCommerce Subscriptions extension and your actual hosting/theme/payment-plugin combination still require hands-on testing. Automated tests are not the owner's final approval. Before enabling [Live](/docs/v1/start/glossary#live): 1. Complete a Sandbox order using your actual store payment plugin's test mode. A Sandbox KDPika connection does not automatically switch the store's payment gateway to test mode. 2. Verify the buyer receives the correct key through your chosen delivery channels. 3. Repeat notifications, retry a temporary interruption and verify that no second key appears. 4. Refund one unit and a full order; confirm unrelated items retain access. 5. If using WooCommerce Subscriptions, test initial payment, ordinary/early renewal, failure/grace/recovery, cancellation, expiration and a plan switch. Do not offer a lifecycle that remains unverified for your store. 6. Confirm least-privilege product permissions, restore/revoke behavior, privacy and backup policies. 7. Connect the store to **Live** only when its own payments and your eligible KDPika plan are ready. Existing Sandbox licenses do not become Live by changing a selector. ## Legacy integrations The previous [developer-operated service reference](/docs/v1/guides/woocommerce/legacy-service) remains available for existing installations. It is not required for the official plugin. Do not run two independent issuers for the same WooCommerce order. ## Expected result One approved license entitlement per eligible purchased unit, safely reused after duplicate notifications or retries. The buyer finds the correct private key through your WooCommerce store, without creating a KDPika account. ## Common mistakes Do not use a Live payment gateway just because the KDPika connection says Sandbox. Do not install both the official plugin and the legacy issuer for the same order. Map variations individually, retain saved order mappings, and never delete the recovery queue to hide a failed operation. An API receipt is not proof of license delivery or inbox placement. ## Troubleshooting If connection fails, confirm the exact public HTTPS WordPress address, administrator permissions, allowed software products and matching environment. For queued orders, use Diagnostics and the per-order recovery actions described above. If no payment or license action is available, contact [hi@kdpika.com](mailto:hi@kdpika.com) with the non-sensitive order reference and error code; never include keys, credentials or buyer payment data. ## Next steps Follow the Sandbox and Live checklist above with your actual store. Test your chosen email templates and customer-account setup before inviting buyers. Owner hands-on approval is still required before this candidate is treated as finally accepted. ## Related - [Sandbox quickstart](/docs/v1/start/quickstart) - [Legacy service reference](/docs/v1/guides/woocommerce/legacy-service) - [Privacy policy](/legal/privacy) - [Data processing agreement](/legal/dpa) # Sell with Shopify Canonical: https://licensing.kdpika.com/docs/v1/guides/shopify --- title: "Sell with Shopify" description: "Verified Shopify purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/shopify" section: "Webhooks and integrations" order: 75 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own Shopify store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Configure an app for your own development shop and a server-only Admin API token. Request order access and the product/[customer](/docs/v1/start/glossary#customer) field access needed by the query; protected customer data approval may be required for email/name. Do not request unrelated write permissions. 2. Pin API version 2026-10. Register orders/paid, refunds/create and orders/cancelled [webhook](/docs/v1/start/glossary#webhook) subscriptions for your own HTTPS /provider-event endpoint. 3. Set KDP_STORE_API_KEY to the app's private Admin token and KDP_STORE_WEBHOOK_SECRET to that app's client secret. The token and secret must belong to the same app/shop. 4. Set provider/source to shopify, shop_domain to the exact lowercase YOUR_SHOP.myshopify.com domain and seller_id to a stable private merchant reference. 5. Map exact gid://shopify/ProductVariant/ IDs in offer_map. Enable Test payments in the development shop and start with KDPika Sandbox. 6. Use the complete Node ingress linked below in front of the shared PHP service, or route Shopify directly to the PHP endpoint. Never expose the Admin token in a storefront, theme or distributed app. ## Verify before granting access X-Shopify-Hmac-Sha256 is base64 HMAC-SHA256 over raw bytes. The endpoint additionally pins X-Shopify-Shop-Domain and allows only the registered topics. The [delivery](/docs/v1/start/glossary#delivery) ID deduplicates the inbox; the order ID deduplicates issuance across deliveries. Admin GraphQL queries the pinned shop, refuses errors/missing protected data and checks Order.test against the configured environment. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | orders/paid | Current order PAID, not cancelled; one quantity-one mapped variant; no additional line page | Issue once using the authenticated order email. | | refunds/create | Refund ID is on that current order and displayFinancialStatus is REFUNDED | Revoke the existing order license. A partial refund requires review. | | orders/cancelled | Current cancelledAt is present | Record cancellation intent without treating it as a confirmed refund. | The exact Admin GraphQL query was validated against Shopify 2026-10's schema; this is not a query executed against an installed merchant account. Order access [scopes](/docs/v1/start/glossary#scope) have alternatives depending on the application type. Check the fields actually authorized for your installation; missing email must hold fulfillment, not silently choose another recipient. Historical orders beyond ordinary access windows may need separately approved order access. This guide covers orders and refunds, not Shopify selling-plan subscription contracts. A new recurring order is an independent order here; automatic renewal of the original KDPika entitlement requires an authoritative subscription-contract adapter that is not supplied. Do not claim that every app's subscription lifecycle or every multi-item cart is covered. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php api)('POST', '/admin/api/2026-10/graphql.json', ['query' => $query, 'variables' => ['id' => 'gid://shopify/Order/' . rawurldecode($this->ref($orderId))]]); storeRequire(empty($result['errors']) && is_array($result['data']['order'] ?? null), 'ORDER_NOT_CONFIRMED'); $order = $result['data']['order']; $this->mode(isset($order['test']) ? !$order['test'] : null); $id = (string) $orderId; if ($topic === 'refunds/create') { $refundId = 'gid://shopify/Refund/' . rawurldecode($this->ref($event['id'] ?? null)); storeRequire(in_array($refundId, array_column($order['refunds'] ?? [], 'id'), true) && ($order['displayFinancialStatus'] ?? '') === 'REFUNDED', 'PARTIAL_OR_UNKNOWN_REFUND_REQUIRES_REVIEW'); return [$this->negative($id, $refundId, 'refunded')]; } if ($topic === 'orders/cancelled') { storeRequire(!empty($order['cancelledAt']), 'CANCELLATION_NOT_CONFIRMED'); return [$this->negative($id, 'cancel:' . $id, 'cancelled')]; } storeRequire(($order['displayFinancialStatus'] ?? '') === 'PAID' && empty($order['cancelledAt']) && ($order['lineItems']['pageInfo']['hasNextPage'] ?? true) === false, 'ORDER_NOT_CURRENTLY_PAID'); $line = $this->one($order['lineItems']['nodes'] ?? []); $variant = (string) ($line['variant']['id'] ?? ''); $customer = $order['customer'] ?? []; return [$this->paid($id, $id, $variant, (string) ($order['email'] ?? ''), trim(($customer['firstName'] ?? '') . ' ' . ($customer['lastName'] ?? '')), false)]; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real Shopify account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before [Live](/docs/v1/start/glossary#live) use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://shopify.dev/docs/apps/build/webhooks/verify-deliveries) - [Official reference 2](https://shopify.dev/docs/api/admin-graphql/2026-10/queries/order) - [Official reference 3](https://shopify.dev/docs/apps/launch/protected-customer-data) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Sell with PayPal Canonical: https://licensing.kdpika.com/docs/v1/guides/paypal --- title: "Sell with PayPal" description: "Verified PayPal purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/paypal" section: "Webhooks and integrations" order: 76 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own PayPal store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Create a PayPal Sandbox REST app for your own merchant and a Sandbox buyer. Save the merchant ID and the [webhook](/docs/v1/start/glossary#webhook) ID registered for this same app. 2. Configure your own HTTPS /provider-event destination for PAYMENT.CAPTURE.COMPLETED, PAYMENT.CAPTURE.REFUNDED, PAYMENT.SALE.COMPLETED, PAYMENT.SALE.REFUNDED, BILLING.SUBSCRIPTION.SUSPENDED, BILLING.SUBSCRIPTION.PAYMENT.FAILED, BILLING.SUBSCRIPTION.CANCELLED, BILLING.SUBSCRIPTION.EXPIRED and [CUSTOMER](/docs/v1/start/glossary#customer).DISPUTE.CREATED/UPDATED/RESOLVED. 3. Set provider/source to paypal, environment to sandbox, seller_id to the merchant ID and webhook_id to the registered webhook ID. Set private KDP_STORE_CLIENT_ID and KDP_STORE_CLIENT_SECRET; leave KDP_STORE_API_KEY empty to obtain an OAuth token automatically. A privately rotated Bearer token is an alternative. 4. One-time orders must contain one purchase unit, one quantity-one item with a mapped sku and one capture. Map the sku in offer_map. Recurring subscriptions map the plan_id instead. 5. Make the capture/subscription payment in your own trusted backend. Do not treat buyer approval, an activated subscription without a paid transaction, or a browser return as collection. ## Verify before granting access The reference submits the actual webhook event, transmission headers and configured webhook ID to PayPal verify-webhook-signature with its private OAuth token. Only verification_status SUCCESS is accepted. It never downloads an arbitrary cert_url itself. Sandbox and [Live](/docs/v1/start/glossary#live) use distinct fixed API origins and app credentials. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | PAYMENT.CAPTURE.COMPLETED | Current completed capture belongs to a completed one-unit order, expected payee and one mapped sku | Issue once by order, retaining capture mapping for refunds/disputes. | | PAYMENT.SALE.COMPLETED | Active current subscription and matching COMPLETED transaction in authenticated subscription transaction history | Issue initial purchase or renew existing subscription once by sale ID. | | BILLING.SUBSCRIPTION.SUSPENDED / PAYMENT.FAILED / CANCELLED / EXPIRED | Current subscription status is SUSPENDED / CANCELLED / EXPIRED | Suspend / preserve paid time and record cancellation / suspend. A failure hint while still ACTIVE does not prove suspension. | | PAYMENT.CAPTURE.REFUNDED | Current completed refund and original capture fully REFUNDED | Revoke mapped license. Partial refunds require review. | | PAYMENT.SALE.REFUNDED | Current completed native sale refund and original sale fully refunded | Revoke subscription entitlement linked to the sale. This is not a v2 capture refund. | | CUSTOMER.DISPUTE.CREATED / UPDATED / RESOLVED | Current dispute has one transaction for the expected merchant | Suspend while disputed; confirmed seller-favour outcome restores eligible, nonterminal access. | PayPal subscription payments still expose sale IDs. The refund verification reads the documented native sale/refund resources; it does not create deprecated v1 payments or pretend these IDs are v2 captures. Confirm these read permissions and lifecycle notifications on your own PayPal app before Live use. The verification API call is online; a timeout is not a valid signature. The worker requests the narrow transaction time window around the native payment timestamp and requires an exact transaction ID match. No unsupported sandbox flag is invented inside a capture; fixed account origin/app credentials establish the configured mode. A new paid subscription without a prior local mapping still needs your verified initial-purchase policy. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php get('/v2/payments/captures/' . $this->ref($resource['id'] ?? null)); storeRequire(($capture['status'] ?? '') === 'COMPLETED', 'CAPTURE_NOT_COMPLETED'); $orderId = $capture['supplementary_data']['related_ids']['order_id'] ?? null; $order = $this->get('/v2/checkout/orders/' . $this->ref($orderId)); storeRequire(($order['status'] ?? '') === 'COMPLETED', 'ORDER_NOT_COMPLETED'); $unit = $this->one($order['purchase_units'] ?? []); storeRequire(($unit['payee']['merchant_id'] ?? null) === $this->config['seller_id'], 'WRONG_PAYEE'); $item = $this->one($unit['items'] ?? []); $captures = $unit['payments']['captures'] ?? []; storeRequire(count($captures) === 1 && ($captures[0]['id'] ?? null) === ($capture['id'] ?? null) && ($captures[0]['status'] ?? '') === 'COMPLETED', 'CAPTURE_ORDER_MISMATCH'); return [$this->paid((string) $orderId, (string) $capture['id'], (string) ($item['sku'] ?? ''), (string) ($order['payer']['email_address'] ?? ''), trim(($order['payer']['name']['given_name'] ?? '') . ' ' . ($order['payer']['name']['surname'] ?? '')), false, [(string) $capture['id']])]; } if ($type === 'PAYMENT.SALE.COMPLETED') { $subId = $resource['billing_agreement_id'] ?? null; $subscription = $this->get('/v1/billing/subscriptions/' . $this->ref($subId)); storeRequire(($subscription['status'] ?? '') === 'ACTIVE', 'SUBSCRIPTION_NOT_ACTIVE'); $created = $resource['create_time'] ?? null; storeRequire(is_string($created) && strtotime($created) !== false, 'PAYMENT_TIME_REQUIRED'); $start = gmdate('Y-m-d\TH:i:s\Z', strtotime($created) - 60); $end = gmdate('Y-m-d\TH:i:s\Z', strtotime($created) + 60); $transactions = $this->get('/v1/billing/subscriptions/' . $this->ref($subId) . '/transactions?start_time=' . rawurlencode($start) . '&end_time=' . rawurlencode($end)); $matched = array_values(array_filter($transactions['transactions'] ?? [], static fn(array $item): bool => ($item['id'] ?? null) === ($resource['id'] ?? null) && ($item['status'] ?? '') === 'COMPLETED')); storeRequire(count($matched) === 1, 'SUBSCRIPTION_PAYMENT_NOT_CONFIRMED'); $subscriber = $subscription['subscriber'] ?? []; return [$this->paid((string) $subId, (string) $resource['id'], (string) ($subscription['plan_id'] ?? ''), (string) ($subscriber['email_address'] ?? ''), trim(($subscriber['name']['given_name'] ?? '') . ' ' . ($subscriber['name']['surname'] ?? '')), true, [(string) $resource['id']])]; } if ($type === 'PAYMENT.SALE.REFUNDED') { // Billing subscription transactions use sale IDs, not v2 capture IDs. // Read the native refund and original sale; never initiate a v1 payment. $refund = $this->get('/v1/payments/refund/' . $this->ref($resource['id'] ?? null)); storeRequire(($refund['state'] ?? '') === 'completed', 'REFUND_NOT_COMPLETED'); $saleId = $refund['sale_id'] ?? null; $sale = $this->get('/v1/payments/sale/' . $this->ref($saleId)); storeRequire(($sale['id'] ?? null) === $saleId && ($sale['state'] ?? '') === 'refunded', 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) $saleId, (string) $event['id'], 'refunded')]; } if (str_starts_with($type, 'BILLING.SUBSCRIPTION.')) { $subscription = $this->get('/v1/billing/subscriptions/' . $this->ref($resource['id'] ?? null)); $state = match ($subscription['status'] ?? '') { 'SUSPENDED' => 'failed', 'CANCELLED' => 'cancelled', 'EXPIRED' => 'ended', default => null }; return $state === null ? [] : [$this->negative((string) $subscription['id'], (string) $event['id'], $state)]; } if ($type === 'PAYMENT.CAPTURE.REFUNDED') { $refund = $this->get('/v2/payments/refunds/' . $this->ref($resource['id'] ?? null)); storeRequire(($refund['status'] ?? '') === 'COMPLETED', 'REFUND_NOT_COMPLETED'); $captureId = $resource['supplementary_data']['related_ids']['capture_id'] ?? null; // Native refund events can omit capture_id; use the API's fixed-origin up link. if (!is_string($captureId)) { foreach ($refund['links'] ?? [] as $link) { if (($link['rel'] ?? '') === 'up' && preg_match('~/v2/payments/captures/([A-Za-z0-9_-]+)$~D', (string) ($link['href'] ?? ''), $match)) { $captureId = $match[1]; } } } $capture = $this->get('/v2/payments/captures/' . $this->ref($captureId)); storeRequire(($capture['status'] ?? '') === 'REFUNDED', 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) $captureId, (string) $event['id'], 'refunded')]; } if (str_starts_with($type, 'CUSTOMER.DISPUTE.')) { $dispute = $this->get('/v1/customer/disputes/' . $this->ref($resource['dispute_id'] ?? null)); $transaction = $this->one($dispute['disputed_transactions'] ?? []); storeRequire(($transaction['seller']['merchant_id'] ?? null) === $this->config['seller_id'], 'WRONG_DISPUTE_MERCHANT'); $state = ($dispute['dispute_outcome']['outcome_code'] ?? '') === 'RESOLVED_SELLER_FAVOUR' ? 'recovered' : 'disputed'; return [$this->negative((string) ($transaction['seller_transaction_id'] ?? ''), (string) $event['id'], $state)]; } return []; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real PayPal account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before Live use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://developer.paypal.com/api/webhooks/v1/) - [Official reference 2](https://developer.paypal.com/api/subscriptions/v1/) - [Official reference 3](https://developer.paypal.com/api/payments/v2/) - [Official reference 4](https://developer.paypal.com/api/deprecated/payments/v1/refund-get) - [Official reference 5](https://developer.paypal.com/api/deprecated/payments/v1/sale-get) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Sell with FastSpring Canonical: https://licensing.kdpika.com/docs/v1/guides/fastspring --- title: "Sell with FastSpring" description: "Verified FastSpring purchase-to-license reference with native proof, durable mapping and honest lifecycle boundaries." slug: "/docs/v1/guides/fastspring" section: "Webhooks and integrations" order: 77 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Connect your own FastSpring store to [KDPika licensing](/docs/v1/start/glossary#kdpika-licensing) through a verified, durable backend. This does not add a processor to KDPika [platform subscriptions](/docs/v1/start/glossary#platform-subscription). ## What you will learn - Configure native notifications and approved [product](/docs/v1/start/glossary#product)/plan mappings. - Issue once after confirmed payment, then handle supported lifecycle changes. - Deliver a saved key privately and test without exposing buyer/card data. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart) and [store service setup](/docs/v1/guides/store-service). - Use your own provider merchant account and private backend credentials; never KDPika's platform billing credentials. - Have a product-scoped Sandbox management key with [licenses](/docs/v1/start/glossary#license):read and licenses:write. - Configure one eligible quantity-one line. Mixed baskets, multi-seat quantities and partial refunds need an explicitly reviewed policy. ## Configure the provider 1. Use your own FastSpring test storefront and create one quantity-one product. Save its exact product path. 2. Create an API credential with read access to your orders, subscriptions and returns. Save its username/password privately as KDP_STORE_API_KEY and KDP_STORE_API_SECRET. 3. Configure an HMAC-secured [webhook](/docs/v1/start/glossary#webhook) endpoint at your own HTTPS /provider-event. Save its secret as KDP_STORE_WEBHOOK_SECRET. Select order.completed, subscription.charge.completed, subscription.charge.failed, subscription.canceled, subscription.deactivated, subscription.updated and return.created as supported in your account. 4. Set provider/source to fastspring, environment to sandbox and seller_id to your stable merchant reference. The API credential and webhook must belong to this same merchant. 5. Map each exact product path in offer_map. Native API objects' [live](/docs/v1/start/glossary#live) fields must match the configured environment. Keep a separate state/configuration for each account and environment. ## Verify before granting access X-FS-Signature is base64 HMAC-SHA256 over exact raw bytes. A [signed envelope](/docs/v1/start/glossary#signed-envelope) can contain up to 50 events here: each event is committed separately within one durable batch receipt. Event IDs deduplicate deliveries; stable order IDs prevent double granting when provider retries use different event IDs. Current objects are read with private Basic API authentication. Provider verification, receipt and fulfillment are separate. HTTP 202 only means the authenticated notification was saved. A buyer redirect or successful browser message never creates a license. Only the backend worker can authorize the scoped licensing write after current provider reads. ## Event and lifecycle mapping | Notification | Confirmed evidence | Licensing decision | | --- | --- | --- | | order.completed | Current completed order, matching live mode, one mapped product, active matching subscription if present | Issue initial order once. Subscription identity is verified against its current record. | | subscription.charge.completed | Current completed order and active subscription; GROUP_REGULAR_PERIOD acquisition type | Renew existing subscription once by order ID. Prorated charge types require review. | | subscription.charge.failed / subscription.updated | Current subscription is overdue, not merely an old failure notification | Suspend. A healthy recovered subscription is not suspended by a stale failure. | | subscription.canceled / subscription.deactivated | Current canceled intent or inactive subscription | Preserve paid time on cancellation; ended/inactive suspends. | | return.created | Current completed return with one Full Refund item and matching live mode | Revoke license linked to original order. Partial/unknown returns require review. | The orders/{id} and subscriptions/{id} endpoints return a single object; returns/{id} uses a returns array. The reference follows those different documented shapes. Subscription charge events can include prorations, so event name alone is insufficient. Refund-before-purchase mapping is held for reconciliation and remembered as an adverse-reference tombstone, not silently discarded. There is no invented universal chargeback event or automatic in-place plan migration. ## Server examples and deployment - [Complete PHP service and hosting setup](/docs/v1/guides/store-service) - [Complete Node.js ingress](/docs/v1/guides/store-service/node) The PHP application contains the native verification, provider API reads, durable ledger and licensing worker. Optional Node/Python examples are complete raw-body ingress servers with an explicit PHP service dependency, not independent native payment workers. The PHP shared-hosting path needs neither Node nor a long-running process. 1. Download [all exact sources](/docs/downloads/store-integrations.zip) and check the [SHA-256 manifest](/docs/store-examples.json). 2. Follow the shared setup without changing the provider/module source or putting private config under a web root. 3. Set config.json provider/merchant/mode and offer_map; set fulfillment.json source to the identical provider string and map the approved internal offer to your [product plan](/docs/v1/start/glossary#product-plan). 4. Run the private work cron, inspect its aggregate states, and then run the separate mail cron. Keep both ledgers/backups persistent. 5. Configure [SMTP delivery](/docs/v1/guides/store-service/email) for your own sender. It reuses the encrypted saved key; it never creates another license to resend. ## Complete provider module This exact module is loaded by the [shared normalizer](/docs/v1/guides/store-service/normalizer). It depends on the complete [native authentication and HTTP client](/docs/v1/guides/store-service/security) and [durable service source](/docs/v1/guides/store-service/worker). It is not a standalone copy/paste webhook endpoint. ```php get('/orders/' . $this->ref($orderId)); $this->mode($order['live'] ?? null); storeRequire(($order['id'] ?? null) === $orderId, 'ORDER_ID_MISMATCH'); storeRequire(($order['completed'] ?? false) === true, 'ORDER_NOT_COMPLETED'); $item = $this->one($order['items'] ?? []); $nativeOrder = is_array($data['order'] ?? null) ? $data['order'] : $data; $subscription = $data['subscription'] ?? $nativeOrder['items'][0]['subscription'] ?? null; if (is_array($subscription)) { $subscription = $subscription['id'] ?? null; } $recurring = is_string($subscription); storeRequire($type !== 'subscription.charge.completed' || $recurring, 'SUBSCRIPTION_MAPPING_REQUIRED'); if ($recurring) { $currentSubscription = $this->get('/subscriptions/' . $this->ref($subscription)); $this->mode($currentSubscription['live'] ?? null); storeRequire(($currentSubscription['active'] ?? false) === true && in_array($currentSubscription['state'] ?? '', ['active', 'trial'], true) && ($currentSubscription['product'] ?? null) === ($item['product'] ?? null), 'SUBSCRIPTION_NOT_ACTIVE'); } if ($type === 'subscription.charge.completed') { storeRequire(($nativeOrder['acquisitionTransactionType'] ?? '') === 'GROUP_REGULAR_PERIOD', 'PRORATED_CHARGE_REQUIRES_REVIEW'); } $customer = $order['customer'] ?? []; return [$this->paid((string) ($subscription ?? $orderId), (string) $orderId, (string) ($item['product'] ?? ''), (string) ($customer['email'] ?? ''), trim(($customer['first'] ?? '') . ' ' . ($customer['last'] ?? '')), $recurring, [(string) $orderId], null, $type === 'order.completed')]; } if (str_starts_with($type, 'subscription.')) { $id = $data['subscription'] ?? $data['id'] ?? null; if (is_array($id)) { $id = $id['id'] ?? null; } $subscription = $this->get('/subscriptions/' . $this->ref($id)); $this->mode($subscription['live'] ?? null); $state = match (true) { ($subscription['active'] ?? true) === false => 'ended', ($subscription['state'] ?? '') === 'canceled' || !empty($subscription['canceledDate']) => 'cancelled', ($subscription['state'] ?? '') === 'overdue' => 'failed', default => null }; return $state === null ? [] : [$this->negative((string) $id, (string) $event['id'], $state)]; } if ($type === 'return.created') { $id = $data['id'] ?? null; $result = $this->get('/returns/' . $this->ref($id)); $return = $this->one($result['returns'] ?? []); storeRequire(($return['return'] ?? null) === $id && ($return['completed'] ?? false) === true, 'RETURN_NOT_COMPLETED'); $this->mode($return['live'] ?? null); $item = $this->one($return['items'] ?? []); storeRequire(($item['refundType'] ?? '') === 'Full Refund', 'PARTIAL_REFUND_REQUIRES_REVIEW'); return [$this->negative((string) ($return['original']['id'] ?? ''), (string) $event['id'], 'refunded')]; } return []; } } ``` ## Test in Sandbox 1. Confirm invalid/missing proof cannot issue, and no key or API credential appears in the HTTP reply. 2. Complete one permitted Test purchase in your merchant account. Confirm the provider delivery, then run work and verify one DONE issue and a saved encrypted key. 3. Deliver to your verified test inbox and activate that key with the same buyer email and Sandbox App ID. Test a mismatched email too. 4. Redeliver the same event and overlapping supported paid events. Confirm no second key and no additional renewal days for the same paid receipt. 5. For supported subscriptions, test initial payment, another confirmed paid period, current failure/recovery and cancellation. Verify cancellation does not remove already-paid time. 6. Test a supported full refund and any exposed dispute lifecycle. Check revoked/suspended state in KDPika; never simulate a refund only by editing the browser return URL. 7. Test stale events and a missing initial mapping. Unknown, partial or unsupported cases must be held for review, not silently fulfilled. Expected result: one authorized initial license, one additive extension per supported new paid receipt, no duplicate grant, no access from an unverified event, and a private delivery record. Renewal adds your mapped integer days, not an exact calendar-period-end update. ## Verification boundary The exact source was executed through local HTTP ingress and explicit synthetic provider-schema API fixtures, with real isolated KDPika Sandbox issuance, [activation](/docs/v1/start/glossary#activation) and supported lifecycle writes. Fixture signatures use local test secrets, not a provider's private signing infrastructure. No real FastSpring account purchase is claimed. Your account's permissions, actual delivery format, TLS, provider checkout and email inbox still need the procedure above before Live use. Native WordPress/merchant account execution was unavailable in this documentation environment; this is the explicit contract-test boundary, not recorded-payment evidence. ## Go-live checklist - Review the unsupported boundaries above; do not sell a lifecycle this adapter does not implement. - Complete your provider account verification and real Test delivery procedure; check merchant, recipient, item and mode. - Use a separate Live config/state and scoped Live KDPika key; set KDP_CONFIRM_LIVE only after review. Never turn existing Sandbox ledgers into Live by editing their binding. - Replace provider credentials/destination secret with the matching Live account values. Do not reuse example placeholders. - Remove every loopback test override, use verified HTTPS and keep credentials and SQLite/encryption backups outside all web roots. - Monitor private REVIEW/IN_FLIGHT and mail failures. A missing acknowledgement is not proof a write or email did not happen; reconcile before retrying. - Retain business-effect identities to prevent reissuance. Apply your documented retention, refund, support and key-redelivery policies. ## Common mistakes - Shipping a server management key inside an application or theme. - Issuing from a redirect, subscription-created event or unverified notification. - Granting both initial checkout and initial recurring invoice independently. - Deleting the durable ledger or creating a new effect ID after a timeout. - Treating a saved key or SMTP acceptance as confirmed inbox delivery. ## Official sources Reviewed for this reference on 18 October 2026. Provider documentation can change independently; check your pinned API and account before upgrading. - [Official reference 1](https://developer.fastspring.com/reference/message-security) - [Official reference 2](https://developer.fastspring.com/reference/retrieve-an-order) - [Official reference 3](https://developer.fastspring.com/reference/retrieve-a-subscription) - [Official reference 4](https://developer.fastspring.com/reference/retrieve-a-return) - [Official reference 5](https://developer.fastspring.com/reference/subscription-charge-completed) ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Connect your own store backend Canonical: https://licensing.kdpika.com/docs/v1/guides/any-store --- title: "Connect your own store backend" description: "Map verified store purchases to durable licensing commands with four tested reference servers and conservative unknown-result handling." slug: "/docs/v1/guides/any-store" section: "Webhooks and integrations" order: 78 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/api/create-license","/docs/v1/api/idempotency","/docs/v1/guides/manual-issuance"] draft: false --- Translate a verified purchase into a durable backend command, then call the existing licensing API once for that authorized effect. ## What you will learn - Separate payment-provider verification from licensing fulfillment. - Configure approved offer mappings without accepting a plan from a browser. - Run a complete Node.js, Python, PHP or C# reference receiver and worker. - Preserve a paid [customer](/docs/v1/start/glossary#customer)'s access and stop safely after an uncertain result. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart). - Read [create license](/docs/v1/api/create-license) and [idempotency](/docs/v1/api/idempotency), including the known replay limitations. - Create a [product](/docs/v1/start/glossary#product) and plan; keep their ID or slug mapping privately. - Use a backend [developer](/docs/v1/start/glossary#developer) key with licenses:read and licenses:write for that product and environment. - Have a trusted store backend that can verify the provider's real purchase, refund and subscription state. - Keep private state, backups and exports outside every web root; start in Sandbox. ## Separate the three contracts | Boundary | Who authenticates it | What success proves | | --- | --- | --- | | Store/provider → your adapter | Your adapter uses that provider's actual signature/API verification and confirms merchant, environment, recipient, purchased offer and current state | A particular business effect is authorized; not that licensing completed | | Your adapter → reference receiver | Separate private command HMAC, approved mapping and durable effect/sequence checks | HTTP 202 means durable receipt, not payment confirmation, license issuance or an email | | Reference worker → KDPika | Scoped backend developer key, existing API v1, explicit accepted response | The corresponding license action completed; a full new key is encrypted locally for [delivery](/docs/v1/start/glossary#delivery) | KDPika does not process payments from your software buyers. The platform's own billing integration is not your store adapter. A browser return URL, success parameter, unsigned JSON field or customer-selected plan is never payment evidence. The private format below is not a new public KDPika endpoint or a native event format from a provider. License [outbound notifications](/docs/v1/guides/webhooks-overview) are a fourth, separate flow. Their X-LMP-Signature must not be treated as a payment-provider signature or reused as this command secret. ## Choose the complete reference implementation :::cards - [Node.js reference](/docs/v1/guides/any-store/node) - [Python reference](/docs/v1/guides/any-store/python) - [PHP reference](/docs/v1/guides/any-store/php) - [C# reference](/docs/v1/guides/any-store/csharp) ::: Download the [complete source archive](/docs/downloads/store-fulfillment.zip). It contains all four servers, project/dependency files, the private protocol, example mapping, example command and tested command sender. The [source manifest](/docs/fulfillment-examples.json) records the archive and member SHA-256 values. No state database, credentials, buyer files, binaries or exported keys are included. All four implementations run on POSIX macOS/Linux-oriented backends. They are not native Windows ACL/lock examples. Your selected hosting must support the chosen runtime and private persistent SQLite filesystem. PHP shared hosting can use the PHP implementation; the other runtimes are not requirements for KDPika's hosting. ## Configure an approved offer mapping 1. Copy config.example.json to an absolute private file outside your web root. 2. Replace source and seller_id with stable nonpersonal IDs identifying your store and merchant account. 3. Keep environment as sandbox for this test. 4. Set base_url to the HTTPS platform origin, without /api/v1, a path, query or fragment. 5. Replace product_id with your saved product reference. 6. Replace each offer key with an independently verified provider price/item reference and its plan_id. 7. Choose renewal_days deliberately for that offer's renewal policy. ```json { "source": "your-store", "seller_id": "YOUR_STORE_ACCOUNT", "environment": "sandbox", "base_url": "https://licensing.kdpika.com", "product_id": "YOUR_PRODUCT_ID", "offers": { "YOUR_VERIFIED_PRICE_ID": { "plan_id": "YOUR_PLAN_ID", "renewal_days": 30 } } } ``` | Mapping field | Trusted source | Reject this alternative | | --- | --- | --- | | source / seller_id | Pinned backend account configuration | Merchant supplied by an unauthenticated caller | | environment | Explicit Sandbox or [Live](/docs/v1/start/glossary#live) adapter configuration and provider mode | A URL parameter used to bypass a mismatch | | product_id / plan_id | Approved product mapping you saved from your workspace | Arbitrary browser-selected KDPika plan | | offer ID | Verified provider object and purchased line | Display name or unverified callback metadata alone | | recipient | Confirmed order/buyer record and your delivery policy | An arbitrary replacement email on a retry | | order ID | Stable purchase line, including subscription line where needed | A fresh random ID each time a [webhook](/docs/v1/start/glossary#webhook) arrives | | effect ID | One durable business action, such as one paid invoice/period | A new ID for every delivery or overlapping event type | | sequence | Your adapter's durable authorized command order | Provider delivery time or incrementing every arrival | Creation uses the selected [product plan](/docs/v1/start/glossary#product-plan)'s stored duration. Renewal adds the mapped integer days; 30 days is not automatically a calendar month, and 365 days is not every calendar year. The existing API has no exact provider-period-end setter or in-place plan-change endpoint. Choose an explicit supported entitlement policy, or use a reviewed replacement-license workflow in [recipes](/docs/v1/guides/recipes). Do not advertise exact period alignment that your mapping cannot provide. ## Keep secrets and state private | Environment variable | Required value | | --- | --- | | KDP_FULFILLMENT_CONFIG | Absolute path to the private mapping JSON | | KDP_FULFILLMENT_STATE | Absolute owner-only persistent directory outside public_html or other web roots | | KDP_FULFILLMENT_SECRET | At least 32 unpredictable characters supplied by your secret store; separate from all provider and outbound notification secrets | | KDP_FULFILLMENT_OUTBOX_KEY | Base64 of 32 random bytes; back up separately from the encrypted ledger | | KDP_API_KEY | Product/environment-scoped backend key; never a client App ID or installation token | | KDP_FULFILLMENT_PORT | Optional Node/Python/C# loopback port; default 8112 | | KDP_CONFIRM_LIVE | Explicit 1 in addition to a Live mapping/key; never set for these tests | | KDP_ALLOW_LOOPBACK | Test-only 1 permits literal HTTP 127.0.0.1 API/receiver URLs in Sandbox; never a public TLS exception | Use your hosting's private environment/secret configuration rather than committing values or pasting them into public support logs. Keep state directory mode 0700 and private files 0600. The implementations refuse broad state permissions and bind the database to the mapping and encryption-key fingerprints. API key rotation does not erase order history. Changing a merchant, product, offer mapping or encryption key needs reviewed reconciliation/migration, not an empty state directory. ## Create an authorized command 1. Verify a provider event using that provider's documented authentication. 2. Fetch or reconcile the actual current payment/order/subscription where the provider requires it. 3. Confirm account ownership, environment, purchased item, payment outcome and recipient. 4. Assign or recover the durable effect ID and command sequence in your adapter's own ledger. 5. Save a complete command to an absolute private JSON file. ```json { "version": 1, "source": "your-store", "seller_id": "YOUR_STORE_ACCOUNT", "environment": "sandbox", "event_id": "YOUR_PROVIDER_EVENT_ID", "effect_id": "YOUR_STABLE_PAID_ORDER_LINE_EFFECT", "order_id": "YOUR_STABLE_ORDER_LINE_ID", "sequence": 1, "action": "issue", "offer_id": "YOUR_VERIFIED_PRICE_ID", "customer": { "email": "buyer@example.invalid", "name": "Demo Buyer" }, "expected_status": null, "expected_expires_at": null } ``` All shown fields are required; unknown fields are rejected. IDs use 1–190 ASCII letters, digits, underscore, period, colon or hyphen. Do not include personal details or keys in IDs. issue requires sequence 1 and null expected fields. Other actions require the recorded expected license status and exact expires_at value, including null if appropriate. Different provider event types can describe the same purchase. For example, a checkout completion and an invoice payment must not each become a separate issuance. Choose one business effect and persist its identity. A second delivery reuses its effect and sequence. Out-of-order commands wait for the missing sequence; assigning a fresh sequence to stale state is not reconciliation. ## Send to the private receiver The supplied sender signs the exact file bytes with X-Fulfillment-Signature. Its message is ASCII UNIX timestamp, one period and the raw body, using HMAC-SHA256. The receiver enforces a 300-second timestamp window, a 262144-byte body limit and exact merchant/environment/mapping checks. These are reference policies, not changes to provider or KDPika contracts. 1. Start your selected reference's loopback receiver as documented on its language page. 2. Configure KDP_FULFILLMENT_URL privately as the receiver's exact /store-event URL. 3. Run the command sender only after the authorization steps above. ```bash php send-command.php /private/verified-command.json ``` Expected receipt: {"accepted":true,"duplicate":false}. An unchanged replay returns duplicate:true. A conflict/refusal returns a nonzero sender exit and a plain instruction to inspect the private receiver; it never retries automatically. HTTP 202 is not a statement that a license or email already exists. PHP/cURL is required only for this supplied sender CLI; the four HTTP receivers accept the same authenticated protocol. A production adapter can implement the exact HMAC privately in its own language. It must preserve durable receipt uncertainty and stable event/effect identities; it must not forward unverified buyer JSON. ## Process and deliver 1. Run work using your selected implementation's CLI or private cron. 2. Inspect its safe processed/states JSON. 3. For DONE issuance, authenticate the intended recipient before exporting the saved key. 4. Run the selected implementation's export command with the stable order ID and a new absolute private output path. 5. Deliver through your private authenticated delivery workflow. 6. Retain the durable business-effect record so a repeated event cannot reissue. Each work run processes at most 25 ready effects and waits 1.1 seconds after an attempted effect. This is a local pacing safeguard, not an exemption from the platform's shared [rate limits](/docs/v1/api/rate-limits). Separate orders can still progress when one order is held. | Action | Behavior | Important boundary | | --- | --- | --- | | issue | Create once with stable effect-derived Idempotency-Key; encrypt the returned full key using AES-256-GCM | A lost successful response cannot be rolled back or safely reissued automatically | | renew | Verify current scoped license, then add the configured integer days once | Unknown result is held; no blind additive retry | | suspend | Apply the eligible server suspension transition | Active installation tokens are revoked | | restore | Apply eligible restoration | Does not revive revoked installation tokens or a terminally revoked license | | revoke | Apply terminal revocation after your verified policy decision | A later dispute/refund reversal does not automatically restore it | | record_cancellation | Save the verified intent without removing paid access | Does not cancel/refund provider billing or schedule future work | The encrypted outbox and explicit export are not an email service. No full key is printed by work/inspect/export. A successful export means a new owner-only key file exists, not that the buyer received it. Reuse the saved key for authenticated delivery; do not create another license to resend. Review [manual key delivery](/docs/v1/guides/manual-issuance#export-a-saved-key-for-delivery) for the corresponding private workflow. ## Reconcile unknown results | State | Meaning | What to do | | --- | --- | --- | | WAITING | Durable receipt awaiting its per-order sequence | Reconcile the missing authorized command; never skip a gap blindly | | IN_FLIGHT | Intent saved before the remote call; interruption may have hidden its result | Hold the order and inspect provider/dashboard evidence before another write | | REVIEW | API/transport/response/state did not meet the accepted result checks | Do not interpret it as proof no license exists; investigate first | | DONE | Explicit accepted result saved with the local completion state | Deliver or inspect; unchanged event replay is harmless | The current API has known create-replay expiration/concurrency limitations and no public license-list/search endpoint to automatically recover an unknown new ID. The local ledger is durable beyond the API's 24-hour replay retention, but cannot guarantee cross-system exactly-once behavior. Keep backups; never delete a held record or invent another order to get a green result. Before a non-issue mutation the worker compares a scoped GET with the authorized expected status/expiration. That read and later write are not atomic compare-and-swap. Keep one reviewed writer for that order; manual dashboard changes or another integration can race it. Stop and reconcile such changes rather than claiming automatic consistency. Node's built-in modules do not expose flock. Its atomic worker.lockdir remains after a killed worker. Confirm the worker is stopped, reconcile held IN_FLIGHT effects, and remove only that reviewed empty lock directory. Never run automatic stale-lock deletion or mix language workers in one state directory. The other three references use POSIX flock. ## Expected result The exact four sources pass actual local Sandbox receiver/worker tests: receipt before issuance, duplicate/conflicting events, signature/mode/mapping refusal, real create/activate/renew/suspend/restore/revoke, durable restart, encrypted export and a gateway that hides a genuinely successful 201. The worker holds the unknown result and sends no second create. This is local application evidence, not a real external provider purchase or SMTP delivery test. The C# reference builds with zero warnings on .NET 10; its NuGet lock records exact direct/transitive versions. Python's exact requirements are executed in an isolated environment. Package-advisory checks cover known database entries at the recorded check time, not a certification or absence of future vulnerabilities. Recheck dependencies and supported runtimes before go-live. ## Common mistakes - Accepting the private example command directly from an untrusted buyer: only your authenticated, verified adapter may authorize it. - Treating a signed provider event as sufficient without confirming merchant, mode and current purchase outcome. - Using a provider delivery ID as the sole business-effect dedupe key when several event types describe one paid purchase. - Cancelling an already-paid license immediately when a subscription only stops renewing. - Deleting the ledger to retry an unknown create, or silently changing offer mappings on an existing state directory. ## Security notes - Use TLS and a reviewed ingress with body limits, timeouts and rate controls before any external deployment. Loopback development servers are not production internet servers. - PHP's SAPI may buffer request bodies before the script starts; configure the web server's 256 KiB limit as well as the application check. Ask the shared host for the correct per-site setting. - Keep full keys, command payloads and recipient email out of web logs, Git, screenshots and analytics. The local ledger contains buyer email/name and needs your retention/access policy. - Back up encrypted state and its key separately. Test recovery; encryption does not replace filesystem isolation or protect a compromised backend that holds both. - Give the backend key only the required product/environment and read/write [scopes](/docs/v1/start/glossary#scope). Never put it in desktop/mobile/browser application code. - Do not add card collection, provider secret configuration or payment callbacks to KDPika to use these examples. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Command not confirmed | Receipt was refused or transport result is unknown | Inspect the existing inbox/effect before any resend; preserve IDs and exact meaning | | 401 | Wrong/duplicate/stale signature or modified body | Check the separate command secret and clock; sign exact bytes after serialization | | 409 | Merchant/environment/mapping or event/effect/order conflict | Correct trusted configuration; reconcile existing records instead of bypassing them | | 413 | Body exceeds the local reference bound | Keep commands small; do not send raw provider histories or attachments | | WAITING never executes | Earlier sequence missing or order held | Reconcile the actual authorized history and held result | | Work exits with safe configuration error | State permissions, key/config drift or busy lock | Inspect private configuration and worker ownership; never publish secrets to debug | | REVIEW after successful provider payment | Licensing result was refused, mismatched or unknown | Keep payment and licensing evidence separate; use request_id/dashboard support to reconcile | | Buyer has no email | No email-delivery workflow was implemented | Deliver the saved key through your private authenticated workflow; do not reissue | ## Next steps Follow your chosen reference page, test Sandbox issuance and refusal cases, then review [licensing recipes](/docs/v1/guides/recipes) before defining renewal and refund policies. ## Related - [API idempotency and known limitations](/docs/v1/api/idempotency) - [Manual issuance and key delivery](/docs/v1/guides/manual-issuance) - [Architectures and trade-offs](/docs/v1/guides/architectures) # Use no-code tools with the REST API Canonical: https://licensing.kdpika.com/docs/v1/guides/no-code --- title: "Use no-code tools with the REST API" description: "Configure reviewed REST issuance in Zapier, Make or n8n, with private credentials, explicit result checks and honest retry/history limits." slug: "/docs/v1/guides/no-code" section: "Webhooks and integrations" order: 79 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/api/create-license","/docs/v1/api/idempotency","/docs/v1/guides/manual-issuance"] draft: false --- Use an optional no-code HTTP action for a reviewed licensing request, not as a shortcut around purchase verification or durable duplicate handling. ## What you will learn - Map one real REST create request into Zapier, Make and n8n. - Keep credentials in a private connection and check the complete result. - Hold uncertain operations instead of silently repeating a mutation. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart). - Create a [product plan](/docs/v1/start/glossary#product-plan) and a Sandbox [developer API key](/docs/v1/start/glossary#developer-api-key) scoped to its [product](/docs/v1/start/glossary#product) with [licenses](/docs/v1/start/glossary#license):write. - Use an existing tool account/instance whose credential, history and retention controls you have reviewed. - Have a verified order or authorized manual request and a durable private operation record. - Read [idempotency limits](/docs/v1/api/idempotency) before enabling any automatic workflow. No Zapier, Make or n8n account is required to use [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing). These are optional tools chosen and operated by the [developer](/docs/v1/start/glossary#developer). This guide does not install another runtime on shared PHP hosting. If you want no external automation service, use the [PHP batch/backend pattern](/docs/v1/guides/manual-issuance) on infrastructure you control. ## Choose a safe scope Start with a single operator-approved Sandbox issuance. The operator verifies the order, reserves its stable order-line operation once in a private ledger, then runs the HTTP action once. The private operation record must survive workflow retries, crashes, tool-history deletion and API replay-window expiry. For unattended purchase fulfillment, you also need authenticated provider input, atomic durable effect reservation, serialized work, encrypted key [delivery](/docs/v1/start/glossary#delivery) and reconciliation after unknown results. A public catch-hook URL, an ordinary spreadsheet lookup or a workflow's success badge does not establish those properties. If your tool/configuration cannot satisfy them, keep it operator-reviewed or call your own reviewed fulfillment backend; do not present direct no-code issuance as safe exactly-once billing automation. The licensing API does not validate a Stripe/Paddle/Gumroad order for you. Use a properly authenticated provider connector or verify the provider event/API result upstream. Never trust a browser-submitted paid=true field, a checkout return URL or an unsigned [webhook](/docs/v1/start/glossary#webhook) as purchase proof. ## Shared request and result settings The complete settings reference below is contract-tested against the actual isolated Sandbox API for each tool profile. It is not an importable vendor workflow, marketplace connector or proof that a tool account was executed. Its explicit placeholders must be replaced with saved product/plan/order/recipient values. ```json { "format": "KDPika no-code HTTP settings reference; not a vendor workflow import", "version": 1, "request": { "method": "POST", "url": "https://licensing.kdpika.com/api/v1/licenses", "credential": { "header": "Authorization", "scheme": "Bearer", "secret_source": "The selected tool's private credential connection", "required_scope": "licenses:write" }, "headers": { "Accept": "application/json", "Content-Type": "application/json", "Idempotency-Key": "YOUR_WORKSPACE_STORE_ORDER_LINE_ID" }, "body": { "product_id": "YOUR_PRODUCT_ID", "plan_id": "YOUR_PLAN_ID", "customer": { "email": "buyer@example.invalid", "name": "Demo Buyer" } } }, "success_checks": { "http_status": 201, "success": true, "required_license_fields": ["id", "license_key", "expires_at", "environment"], "environment": "sandbox" }, "safety": { "automatic_mutation_retry": false, "redirects": false, "verify_tls": true, "persist_intent_before_request": true, "unknown_result_action": "HOLD_FOR_REVIEW", "plaintext_key_in_public_history": false, "provider_event_verification": "Required upstream; an incoming webhook URL is not authentication" }, "tools": [ { "id": "zapier", "action": "API by Zapier: API Request", "credential_location": "Static Headers (API key) connection, Authorization: Bearer YOUR_DEVELOPER_API_KEY", "body_mode": "Valid JSON object", "reference": "https://help.zapier.com/hc/en-us/articles/44391650357005-Send-API-requests-in-Zap-workflows" }, { "id": "make", "action": "HTTP Version 4: Make a request", "credential_location": "API key keychain, Authorization header with Bearer prefix", "body_mode": "application/JSON with Data structure", "reference": "https://apps.make.com/http" }, { "id": "n8n", "action": "HTTP Request", "credential_location": "Generic Credential Type: Header Auth, Authorization: Bearer YOUR_DEVELOPER_API_KEY", "body_mode": "JSON: Using JSON", "reference": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest" } ] } ``` | Setting | Value | | --- | --- | | Method | POST | | URL | https://licensing.kdpika.com/api/v1/licenses | | Authorization | Bearer developer API key, injected by the private credential connection | | Accept and content type | application/json | | Idempotency-Key | Stable [organization](/docs/v1/start/glossary#organization)/store/order-line reference, 8–190 characters, no buyer personal data | | Request fields | product_id, plan_id, [customer](/docs/v1/start/glossary#customer).email and optional customer.name | | Accepted result | HTTP 201, success=true, nonempty license.id/key and expected license.environment=sandbox | | Unknown/error result | Hold the operation; do not automatically repeat create or renew | The key already fixes the product and environment. X-Environment cannot turn a [Live](/docs/v1/start/glossary#live) management key into Sandbox. The create endpoint does not accept an arbitrary expires_at, a store price override or a plan-change action. A product slug/ID is not a [public App ID](/docs/v1/start/glossary#public-app-id). Use saved mappings rather than accepting a buyer-supplied target plan. ## Prepare the durable record 1. Record your verified store account, environment, order and order-line identity. 2. Resolve the intended product/plan from a private allowlisted mapping. 3. Verify the recipient email and any required consent for your delivery workflow. 4. Save the exact normalized request body and stable [idempotency key](/docs/v1/start/glossary#idempotency-key). 5. Reserve that operation so a second worker/operator cannot run it concurrently. 6. Mark the operation IN_FLIGHT before the HTTP step. 7. On explicit accepted success, save the key in a private encrypted outbox and mark DONE. 8. On a timeout, transport failure, malformed response, environment mismatch or HTTP error, mark HOLD_FOR_REVIEW. Expected result: a completed operation is not run again and an unknown outcome is not treated as no license created. A lookup-then-create sequence in two parallel workflow runs is not an atomic reservation. Use a reviewed transactional backend if the tool cannot enforce it. The [tested PHP tool](/docs/v1/guides/manual-issuance) demonstrates why an intent must persist before a network call. ## Zapier ![Zapier procedure: review and reserve one order line, use API by Zapier with private credentials, accept only the expected 201, and hold uncertain results without replay.](/docs/diagrams/p8-no-code-zapier.svg) Zapier procedure: review and reserve one order line, use API by Zapier with private credentials, accept only the expected 201, and hold uncertain results without replay. ### Configure credentials 1. In Apps, add an API by Zapier connection. 2. Select Static Headers (API key). 3. Store Authorization with the value Bearer followed by your Sandbox developer key in that private connection. 4. Restrict Domain filter to licensing.kdpika.com. 5. Give the connection a clear Sandbox/product label. Expected result: the secret is connection-managed rather than pasted into an ordinary action's public configuration. Do not use the public-key endpoint as an authentication test; it succeeds without a developer key. Use a known scoped license GET with a read-scoped key only if you intentionally need a credential test. See the [official connection instructions](https://help.zapier.com/hc/en-us/articles/44391660158733-How-to-get-started-with-API-by-Zapier). ### Configure the request 1. Add API by Zapier and choose API Request. 2. Select the private connection above. 3. Set Method to POST and URL to the exact shared create URL. 4. Add Accept and Content-Type as application/json, plus the saved Idempotency-Key. 5. Put the shared request body in Body and map only approved values. 6. Validate the resulting JSON after mapping; quotes in a buyer name must not corrupt the body. 7. Keep the request under operator review and prevent automatic replay/retry of mutations. 8. Run Test step once for a new synthetic order and inspect the explicit 201/body/environment. 9. Persist the secure outbox and DONE outcome before sending the saved key privately. Expected result: the create response includes a license ID/key and request_id. A step test itself can create a license; pressing it again is not a harmless preview. Prefer connection-managed API by Zapier over storing secrets as plain Webhooks by Zapier step fields. The editor fields are described in the [official API Request instructions](https://help.zapier.com/hc/en-us/articles/44391650357005-Send-API-requests-in-Zap-workflows). Do not use a polling trigger to list KDPika licenses/customers: v1 has no such list endpoint. Do not enable a workflow auto-replay policy for create/renew merely because the tool offers one. If a tool plan does not let you constrain replay and protect returned key history, use your own fulfillment backend instead of direct production issuance. ## Make ![Make procedure: review and reserve one order line, use HTTP Version 4 with a private API-key keychain, accept the expected result and hold incomplete or uncertain runs.](/docs/diagrams/p8-no-code-make.svg) Make procedure: review and reserve one order line, use HTTP Version 4 with a private API-key keychain, accept the expected result and hold incomplete or uncertain runs. ### Configure the HTTP action 1. Use HTTP Version 4 and choose Make a request. 2. Select API key authentication and create a private keychain for the Authorization header, including its Bearer prefix. 3. Set URL to the shared create URL and Method to POST. 4. Add Accept and the saved Idempotency-Key in Headers; keep authentication in the credential field. 5. Select application/JSON as Body content type. 6. Use Data structure for the nested customer object and map only approved product/plan/recipient fields. 7. Set Parse response to Yes. 8. Leave pagination off and do not configure redirecting destinations. 9. Configure failure handling to stop and hold the operation, not resend an uncertain mutation. 10. Run once for a new approved synthetic order, then inspect the expected 201/body/environment. Expected result: JSON mapping preserves its nested customer fields and the returned key goes only to your reviewed private outbox. The [official HTTP module guide](https://apps.make.com/http) documents these settings. A filtered scenario or sequential processing setting alone is not a durable unique order reservation. ### Handle incomplete or retried runs 1. Save HOLD_FOR_REVIEW when the HTTP result is not an explicit accepted success. 2. Inspect the existing operation, request_id and current license/dashboard record before resuming. 3. Do not automatically resolve/retry an incomplete create or additive renew execution. 4. If the platform automatically retries a category of failure and you cannot safely prevent it for this path, send the request to your own durable fulfillment backend rather than directly to the licensing mutation. Expected result: the tool's [incomplete-execution workflow](https://help.make.com/incomplete-executions) does not become a duplicate-issuance mechanism. A stored error record is not proof the upstream API did nothing. Keep credentials and returned customer/key data out of shared scenario screenshots and broadly accessible history. ## n8n ![n8n procedure: review and reserve one order line, use HTTP Request with Header Auth, keep mutation Retry On Fail off and route uncertain results to a held review record.](/docs/diagrams/p8-no-code-n8n.svg) n8n procedure: review and reserve one order line, use HTTP Request with Header Auth, keep mutation Retry On Fail off and route uncertain results to a held review record. ### Configure credentials and HTTP 1. Use an existing reviewed n8n instance/account; this guide does not ask you to install Node or Docker on shared hosting. 2. Create a Header Auth credential with header Authorization and the private Bearer developer-key value. 3. Add an HTTP Request node using Generic Credential Type and select that credential. 4. Set Method to POST and URL to the shared create URL. 5. Enable Send Headers and add Accept, Content-Type and the saved Idempotency-Key. 6. Enable Send Body, choose JSON and Using JSON, and map the approved nested body. 7. Enable Include Response Headers and Status so the 201 can be checked explicitly. 8. Keep Ignore SSL Issues off, Redirects off and Pagination off. 9. Keep Retry On Fail off for create/renew; route a failed or uncertain result to your held review process. 10. Execute once for a fresh approved Sandbox order, then inspect status, success and license.environment. 11. Save the secure key outbox and DONE outcome before any separate key-delivery effect. Expected result: the key is stored in a credential, not workflow JSON, and the HTTP response is not treated as success solely because a node continued. Use the [official HTTP Request settings](https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest) and [HTTP credential guide](https://docs.n8n.io/integrations/builtin/credentials/httprequest/). If Never Error is enabled for explicit routing, check the HTTP status yourself; its output still does not prove a successful license creation. Review execution-data retention and visibility using your [workflow settings](https://docs.n8n.io/build/manage-workflows/configure-workflow-settings.md). Disable unnecessary successful-execution/manual progress retention where appropriate, but do not claim a retention toggle encrypts the returned key or makes email delivery exactly once. Your credential access, execution storage, backups and outbox still need protection. ## Other supported operations | Goal | Supported operation | Safety boundary | | --- | --- | --- | | Inspect a known license | GET /api/v1/licenses/{license} | Requires licenses:read and a known owned ID; not a list/search endpoint | | Temporary payment investigation | POST /api/v1/licenses/{license}/suspend | Requires licenses:write and an authorized current-state decision | | Restore suspended access | POST /api/v1/licenses/{license}/restore | Expiry/state rules apply; old tokens may require reactivation | | Add an approved paid duration | POST /api/v1/licenses/{license}/renew with integer days | Additive, not idempotent; hold uncertain results instead of retrying | | Final revoke | POST /api/v1/licenses/{license}/revoke | Terminal; not a generic refund API or reversible switch | | Release all [devices](/docs/v1/start/glossary#device) | POST /api/v1/licenses/{license}/reset-devices | Requires devices:write; does not reset lifetime [activation count](/docs/v1/start/glossary#activation-count) | Only configure these with their full [API reference](/docs/v1/api/conventions) and verified ownership/state checks. There is no v1 endpoint to charge/refund a store payment, change a license's plan in place, recover its original key or create a buyer-team directory. Do not send a management key from the buyer's browser to a webhook action. ## Test and verification boundary This guide's exact HTTP settings are exercised against the actual isolated Sandbox create/[activation](/docs/v1/start/glossary#activation) API for all three tool profiles. The contract tests cover an explicit create, an identical replay with the same saved body/key, missing credentials and absent idempotency. They are not execution in a connected Zapier, Make or n8n account: no accounts or credentials for those optional services were provided. The procedure diagrams describe verified editor/API settings from official documentation, not fabricated account screenshots. Before enabling a workflow in your own account, test one approved synthetic order, the same order delivered twice, a changed mapping under the same ID, a paused worker/restart, a lost response and a retained key resend. Confirm one durable business effect and zero blind retry on uncertainty. If your workflow cannot meet those checks, keep it manual or use the tested backend pattern. ## Expected result Your selected tool sends the documented API request using a private scoped credential and checks the actual returned state. Unknown outcomes remain held for review, optional tool data exposure is understood, and no unsupported automatic purchase-verification or exactly-once guarantee is implied. ## Common mistakes - Treating an unguessable catch-hook URL as authentication of a payment provider. - Pasting a developer key into ordinary workflow JSON, a shared screenshot or a buyer application. - Using a new Idempotency-Key every time a failed action is replayed. - Sending keys by email before saving the original creation result securely. - Assuming a workflow's dedupe filter protects against concurrent or post-expiration API requests. ## Security notes Optional automation services can see submitted customer data and the complete key returned by create; a private credential connection protects authentication storage, not every response/history copy. Review access, retention, backups and your processor obligations before using real customer data. Do not publish keys in an action output, chat message, public table or link. If you require all fulfillment/key data to remain on your hosting, do not use an external no-code service for this step. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | INVALID_API_KEY or FORBIDDEN | Wrong/revoked credential, product/environment or missing [scope](/docs/v1/start/glossary#scope) | Correct the private connection; do not paste the key into an error ticket | | INVALID_REQUEST on create | Missing stable header, wrong nested JSON or invalid recipient | Compare the exact saved request with the create reference | | IDEMPOTENCY_CONFLICT | Same saved identity with changed decoded request/order | Stop and reconcile the mapping; do not bypass with a new key | | Duplicate license or extra renewal days | Retry, parallel worker or API replay-window assumption | Stop automation and inspect the durable order/effect ledger | | Tool says successful but key delivery is absent | HTTP action and email are separate effects | Check the saved secure outbox; never issue again merely to resend | | Workflow history contains a complete key | Response data was retained by the tool | Restrict/purge under your policy and move issuance to your backend if controls are insufficient | ## Next steps Use [manual issuance and durable key delivery](/docs/v1/guides/manual-issuance) to understand the transaction/recovery boundary, then review the [licensing recipes](/docs/v1/guides/recipes) before adding commercial lifecycle actions. ## Related - [Create license reference](/docs/v1/api/create-license) - [Idempotency limits](/docs/v1/api/idempotency) - [Developer data protection](/docs/v1/security/developer-data-protection) # Issue and deliver licenses manually Canonical: https://licensing.kdpika.com/docs/v1/guides/manual-issuance --- title: "Issue and deliver licenses manually" description: "Issue dashboard licenses, run a safe CSV API batch, find customers and deliver saved keys without inventing import or key-recovery features." slug: "/docs/v1/guides/manual-issuance" section: "Webhooks and integrations" order: 80 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/api/create-license","/docs/v1/api/idempotency"] draft: false --- Issue a [license](/docs/v1/start/glossary#license) after your own purchase or support checks, then deliver its saved key through a private channel. ## What you will learn - Create one license from your workspace. - Run the complete CSV batch tool with durable duplicate prevention and encrypted key storage. - Look up [customers](/docs/v1/start/glossary#customer) and handle a lost key without claiming an unsupported recovery API. ## Prerequisites - Complete the [Sandbox quickstart](/docs/v1/start/quickstart). - Create an active [product](/docs/v1/start/glossary#product) and [product plan](/docs/v1/start/glossary#product-plan) before issuing a license. - For the batch tool, use PHP 8.2+ with cURL, PDO SQLite and Sodium. - Keep all customer files, keys and the issuance state outside your web root. - Confirm the buyer/order independently; this tool does not verify a store payment. ## Create one license in the dashboard 1. Select the intended environment, starting with Sandbox. 2. Open Licenses. 3. Select Create license or Create first license. 4. Select the product and plan in Product and plan. 5. Enter Customer email and, if needed, Customer name, Company and External customer ID. 6. Select Create license. 7. Copy the complete one-time key immediately into your private [delivery](/docs/v1/start/glossary#delivery) workflow. 8. Confirm the row's customer, product, plan, expiration and environment. Expected result: a license and, when necessary, its customer record are created in that environment. The complete key appears once in the success panel. Later rows show only a mask. Creating a license does not charge a customer or automatically email its complete key. A product plan's displayed price is not payment proof. The ordinary dashboard form is not a transactional store-fulfillment queue. Repeating its submit action after an uncertain result can create another license. Inspect existing customer/license rows before issuing again. ## Batch operations that are supported There is no native dashboard CSV importer, bulk-issue endpoint, public customer-list API or public customer-email search API in v1. The downloadable tool below calls the real [create license operation](/docs/v1/api/create-license) once per validated CSV row. It does not change the product database schema. The tool keeps its own private SQLite ledger. It serializes workers with an operating-system file lock, records IN_FLIGHT before a network attempt, and records DONE only after an explicit 201 with the expected environment. The returned key is encrypted using Sodium secretbox before completion is reported. The encryption key stays in your secret store, separate from the ledger and backups. An unknown result becomes REVIEW. An interrupted IN_FLIGHT row is also not retried automatically. This is deliberate: the [current API replay implementation](/docs/v1/api/idempotency) has a 24-hour retention window and known expiration/concurrency limits. Neither this tool nor a provider event ID can turn it into exactly-once delivery. Do not delete the ledger or invent another order ID to bypass a review. ## Prepare the CSV 1. Download and extract the [manual issuance source archive](/docs/downloads/manual-issuance.zip). 2. Create a private UTF-8 CSV with the exact header order below, without a byte-order mark. 3. Use one stable order-line ID per row and one product per batch. 4. Replace the example product and plan references with your saved dashboard IDs or slugs. 5. Use the buyer email you verified in your own store/support workflow. ```text order_id,product_id,plan_id,email,name YOUR_ORDER_LINE_001,YOUR_PRODUCT_ID,YOUR_PLAN_ID,buyer-one@example.invalid,Demo Buyer One YOUR_ORDER_LINE_002,YOUR_PRODUCT_ID,YOUR_PLAN_ID,buyer-two@example.invalid,Demo Buyer Two ``` | Column | Required | Meaning | | --- | --- | --- | | order_id | yes | Stable order-line reference; unique within your workspace/store namespace, not a new random retry ID | | product_id | yes | Product ID or slug matching the API key's product | | plan_id | yes | Active product plan ID or slug within that product | | email | yes | Buyer email used for the license's customer record | | name | column required, value optional | Customer display name | The sample accepts at most 50 rows and a single product per batch. Those are local tool safeguards, not a new [platform plan](/docs/v1/start/glossary#platform-plan) allowance. It validates the whole CSV before sending any issuance request. Use different stable order-line IDs for different purchased items; do not reuse an order ID across several rows or add a customer email to an [idempotency key](/docs/v1/start/glossary#idempotency-key). Product and plan slugs are chosen when you create them. A [public App ID](/docs/v1/start/glossary#public-app-id) is not the create endpoint's product reference. There is no public API for discovering all your product/plan IDs; save the mapping when configuring your integration. ## Validate without issuing ```bash php bulk-issue.php dry-run orders.csv ``` Expected output for the supplied file: {"valid_rows":2,"network_requests":0,"state_writes":0}. This verifies CSV structure, not whether your placeholder product/plan exists or whether payment succeeded. Replace every example value before applying a real batch. ## Configure private issuance state | Environment variable | Value and purpose | | --- | --- | | KDP_API_KEY | Your product-scoped backend [developer API key](/docs/v1/start/glossary#developer-api-key) with licenses:write; never ship it in the buyer application | | KDP_ENVIRONMENT | sandbox initially; must match the key prefix and actual response environment | | KDP_BASE_URL | https://licensing.kdpika.com, without /api/v1, query or fragment | | KDP_BATCH_NAMESPACE | Stable nonpersonal identifier for one [organization](/docs/v1/start/glossary#organization)/store, retained across key rotation and reruns | | KDP_BATCH_STATE | Absolute private owner-only directory outside all web roots | | KDP_OUTBOX_KEY | Base64-encoded 32 random bytes supplied by your secret store; back it up separately | | KDP_CONFIRM_LIVE | Must be 1 in addition to an explicitly configured [live](/docs/v1/start/glossary#live) environment/key; never enable for a test | This POSIX CLI tool is executed on macOS/PHP with SQLite and Sodium. Its absolute-path and permission checks are not a native Windows port. On Windows, use a reviewed WSL/POSIX deployment or adapt and test the path/owner-only ACL checks before running it. No native Windows execution is claimed. The local test suite permits HTTP only when KDP_ALLOW_LOOPBACK=1, the environment is sandbox and the literal host is 127.0.0.1. This is a test-only exception, not a recommendation to send production credentials over HTTP. No unverified redirect is followed. Keep all secret environment values out of shell history and screenshots. ## Apply and read the result ```bash php bulk-issue.php apply orders.csv ``` Expected result: one safe JSON status per processed row, identified by a nonpersonal order hash. DONE includes a license ID and request_id but never the complete key. An unchanged DONE row is skipped on rerun, even beyond the API's replay window. The script waits 1.1 seconds after a successful new create; other traffic can still consume the shared rate bucket, so this is not a rate-limit exemption. | State | Meaning | Next action | | --- | --- | --- | | DONE | Explicit successful creation and encrypted key saved | Deliver the saved key; do not issue again | | IN_FLIGHT | Intent saved, completion unknown after an interruption | Stop and reconcile before any further write | | REVIEW | HTTP, transport, response or environment result was not an accepted 201 | Review safe request_id/error code and dashboard records | | CONFLICT | The stable order reference was reused with different request data | Correct your mapping; do not bypass the record with a new ID | | BATCH_BUSY | Another worker holds the private ledger lock | Wait for that worker; do not run a second state directory | A review or conflict stops the current batch with exit 2; local input/configuration failures exit 1. A complete successful batch exits 0. Even explicit API rejections are held for operator review rather than automatically retried. Do not interpret REVIEW as proof that no license was created. ## Export a saved key for delivery 1. Verify the requester/order and recipient before exporting. 2. Run export with the same environment, namespace, private ledger and outbox key. 3. Send the saved key only through your reviewed private delivery mechanism. 4. Remove the temporary plaintext delivery file when your workflow no longer needs it. ```bash php bulk-issue.php export YOUR_ORDER_LINE_001 /absolute/private/delivery.json ``` Expected result: an owner-only file containing the license ID, complete key and intended customer. Stdout reports exported=true and contains_secret=true without printing the key. Existing export files are not overwritten. Keep the file out of public downloads, analytics, support tickets and Git. A lost outbox encryption key cannot be reconstructed from the encrypted database. ## Complete batch source This exact source is syntax-checked and executed against an isolated Sandbox, including a gateway that loses a genuinely successful create response. The [download manifest](/docs/operation-examples.json) supplies archive and source hashes. ```php =50||count($values)!==5)throw new RuntimeException('INVALID_CSV_SIZE'); $row=array_combine($header,array_map('trim',$values)); if(!preg_match('/^[A-Za-z0-9_.:-]{1,160}$/D',$row['order_id'])||isset($seen[$row['order_id']])|| strlen($row['product_id'])<2||strlen($row['product_id'])>190||strlen($row['plan_id'])<2||strlen($row['plan_id'])>140|| !filter_var($row['email'],FILTER_VALIDATE_EMAIL)||strlen($row['email'])>190||strlen($row['name'])>160)throw new RuntimeException('INVALID_CSV_ROW'); $seen[$row['order_id']]=true;$products[$row['product_id']]=true;$rows[]=$row; } if(!$rows||count($products)!==1)throw new RuntimeException('ONE_PRODUCT_PER_BATCH_REQUIRED'); return $rows; }finally{fclose($stream);} } function batchEnvironment(): string { $value=strtolower((string)getenv('KDP_ENVIRONMENT')); if(!in_array($value,['sandbox','live'],true))throw new RuntimeException('ENVIRONMENT_REQUIRED'); if($value==='live'&&getenv('KDP_CONFIRM_LIVE')!=='1')throw new RuntimeException('LIVE_CONFIRMATION_REQUIRED'); return $value; } function batchDirectory(string $path): string { if(!str_starts_with($path,DIRECTORY_SEPARATOR)||is_link($path))throw new RuntimeException('PRIVATE_ABSOLUTE_DIRECTORY_REQUIRED'); if(!is_dir($path)&&!@mkdir($path,0700,true))throw new RuntimeException('PRIVATE_DIRECTORY_UNAVAILABLE'); if(PHP_OS_FAMILY!=='Windows'&&((int)fileperms($path)&0077)!==0)throw new RuntimeException('DIRECTORY_MUST_BE_OWNER_ONLY'); return rtrim($path,DIRECTORY_SEPARATOR); } function batchKey(): string { $key=base64_decode((string)getenv('KDP_OUTBOX_KEY'),true); if($key===false||strlen($key)!==SODIUM_CRYPTO_SECRETBOX_KEYBYTES)throw new RuntimeException('OUTBOX_KEY_REQUIRED'); return $key; } function batchOrderKey(string $order,string $environment): string { $namespace=(string)getenv('KDP_BATCH_NAMESPACE'); if(!preg_match('/^[A-Za-z0-9_.:-]{1,100}$/D',$namespace))throw new RuntimeException('WORKSPACE_STORE_NAMESPACE_REQUIRED'); return hash('sha256',$namespace."\0".$environment."\0".$order); } function batchDatabase(string $directory): PDO { $path=$directory.'/issuance.sqlite';if(is_link($path))throw new RuntimeException('INVALID_STATE_PATH'); $database=new PDO('sqlite:'.$path,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]); chmod($path,0600); $database->exec('PRAGMA busy_timeout=3000'); $database->exec("CREATE TABLE IF NOT EXISTS issuance(order_hash TEXT PRIMARY KEY,request_hash TEXT NOT NULL,request_json TEXT NOT NULL,state TEXT NOT NULL,license_id TEXT,secret_cipher TEXT,request_id TEXT,http_status INTEGER,error_code TEXT,created_at INTEGER NOT NULL)"); return $database; } function batchConfiguration(string $environment): array { $base=rtrim((string)(getenv('KDP_BASE_URL')?:'https://licensing.kdpika.com'),'/'); $parts=parse_url($base); $local=getenv('KDP_ALLOW_LOOPBACK')==='1'&&$environment==='sandbox'&&($parts['scheme']??'')==='http'&&($parts['host']??'')==='127.0.0.1'; if(!$parts||isset($parts['user'])||isset($parts['pass'])||isset($parts['query'])||isset($parts['fragment'])||!empty($parts['path'])||(!$local&&($parts['scheme']??'')!=='https'))throw new RuntimeException('INVALID_BASE_URL'); $key=(string)getenv('KDP_API_KEY'); if(!str_starts_with($key,'lmp_'.$environment.'_'))throw new RuntimeException('KEY_ENVIRONMENT_MISMATCH'); return [$base,$key,$local]; } function batchRequest(string $json,string $idempotency,string $environment): array { [$base,$key,$local]=batchConfiguration($environment); $raw='';$curl=curl_init($base.'/api/v1/licenses'); curl_setopt_array($curl,[CURLOPT_POST=>true,CURLOPT_POSTFIELDS=>$json,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_CONNECTTIMEOUT=>5,CURLOPT_TIMEOUT=>15, CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2,CURLOPT_PROTOCOLS=>$local?CURLPROTO_HTTP:CURLPROTO_HTTPS, CURLOPT_HTTPHEADER=>['Content-Type: application/json','Accept: application/json','Authorization: Bearer '.$key,'Idempotency-Key: '.$idempotency,'User-Agent: KDPika-Manual-Issuance/1'], CURLOPT_WRITEFUNCTION=>static function($handle,string $bytes)use(&$raw):int{if(strlen($raw)+strlen($bytes)>1048576)return 0;$raw.=$bytes;return strlen($bytes);}]); $sent=curl_exec($curl);$status=(int)curl_getinfo($curl,CURLINFO_RESPONSE_CODE); $body=json_decode($raw,true); return [$sent!==false,$status,is_array($body)?$body:[]]; } function batchApply(array $rows,PDO $database,string $environment,string $outboxKey): int { foreach($rows as $row){ $orderHash=batchOrderKey($row['order_id'],$environment); $json=json_encode(['product_id'=>$row['product_id'],'plan_id'=>$row['plan_id'], 'customer'=>['email'=>$row['email'],'name'=>$row['name']]],JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE|JSON_THROW_ON_ERROR); $requestHash=hash('sha256',$json); $find=$database->prepare('SELECT * FROM issuance WHERE order_hash=?');$find->execute([$orderHash]);$prior=$find->fetch(); if($prior){ $state=hash_equals($prior['request_hash'],$requestHash)?$prior['state']:'CONFLICT'; echo json_encode(['order_hash'=>$orderHash,'state'=>$state,'new_request'=>false]).PHP_EOL; if($state!=='DONE')return 2; continue; } // Commit intent before any network attempt. A crash must never silently reset it. $insert=$database->prepare("INSERT INTO issuance(order_hash,request_hash,request_json,state,created_at) VALUES(?,?,?,'IN_FLIGHT',?)"); $insert->execute([$orderHash,$requestHash,$json,time()]); try{[$transport,$status,$response]=batchRequest($json,'manual_'.$orderHash,$environment);} catch(Throwable){$transport=false;$status=0;$response=[];} $license=$response['license']??null; $valid=$transport&&$status===201&&($response['success']??null)===true&&is_array($license)&& is_string($license['id']??null)&&is_string($license['license_key']??null)&&strlen($license['license_key'])>=12&&strlen($license['license_key'])<=100&&($license['environment']??null)===$environment; $requestId=is_string($response['request_id']??null)?substr($response['request_id'],0,190):null; $code=is_string($response['error']['code']??null)&&preg_match('/^[A-Z_]{1,80}$/D',$response['error']['code'])?$response['error']['code']:null; if(!$valid){ $database->prepare("UPDATE issuance SET state='REVIEW',http_status=?,request_id=?,error_code=? WHERE order_hash=?")->execute([$status,$requestId,$code,$orderHash]); echo json_encode(['order_hash'=>$orderHash,'state'=>'REVIEW','http_status'=>$status,'request_id'=>$requestId,'error_code'=>$code]).PHP_EOL; return 2; } $nonce=random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $cipher=base64_encode($nonce.sodium_crypto_secretbox($license['license_key'],$nonce,$outboxKey)); // Save the returned key encrypted before reporting completion. If this write fails, // IN_FLIGHT remains and the next run refuses a second API call. $database->prepare("UPDATE issuance SET state='DONE',license_id=?,secret_cipher=?,request_id=?,http_status=201 WHERE order_hash=?") ->execute([$license['id'],$cipher,$requestId,$orderHash]); echo json_encode(['order_hash'=>$orderHash,'state'=>'DONE','license_id'=>$license['id'],'request_id'=>$requestId,'new_request'=>true]).PHP_EOL; usleep(1100000); // Below the documented 60 creates/minute per-key bucket, before other traffic. } return 0; } function batchExport(PDO $database,string $order,string $environment,string $key,string $target): void { $find=$database->prepare("SELECT license_id,secret_cipher,request_json FROM issuance WHERE order_hash=? AND state='DONE'"); $find->execute([batchOrderKey($order,$environment)]);$row=$find->fetch();if(!$row)throw new RuntimeException('NO_COMPLETED_DELIVERY'); $encrypted=base64_decode($row['secret_cipher'],true);if($encrypted===false)throw new RuntimeException('INVALID_OUTBOX'); $plain=sodium_crypto_secretbox_open(substr($encrypted,SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),substr($encrypted,0,SODIUM_CRYPTO_SECRETBOX_NONCEBYTES),$key); if($plain===false)throw new RuntimeException('OUTBOX_DECRYPTION_FAILED'); if(!str_starts_with($target,DIRECTORY_SEPARATOR))throw new RuntimeException('PRIVATE_EXPORT_PATH_REQUIRED'); batchDirectory(dirname($target));$stream=@fopen($target,'xb');if(!$stream)throw new RuntimeException('EXPORT_EXISTS_OR_UNAVAILABLE'); try{chmod($target,0600);$customer=json_decode($row['request_json'],true,flags:JSON_THROW_ON_ERROR)['customer']; $json=json_encode(['license_id'=>$row['license_id'],'license_key'=>$plain,'customer'=>$customer],JSON_THROW_ON_ERROR); if(fwrite($stream,$json)!==strlen($json))throw new RuntimeException('EXPORT_WRITE_FAILED'); }finally{fclose($stream);sodium_memzero($plain);} echo '{"exported":true,"contains_secret":true}'.PHP_EOL; } try{ if(($argc!==3||!in_array($argv[1],['dry-run','apply'],true))&&($argc!==4||$argv[1]!=='export'))throw new RuntimeException('USAGE_DRY_RUN_APPLY_OR_EXPORT'); if($argv[1]==='dry-run'){$rows=batchRows($argv[2]);echo json_encode(['valid_rows'=>count($rows),'network_requests'=>0,'state_writes'=>0]).PHP_EOL;exit;} umask(0077);$environment=batchEnvironment();$key=batchKey(); $directory=batchDirectory((string)getenv('KDP_BATCH_STATE')); $lock=@fopen($directory.'/worker.lock','c');if(!$lock||!flock($lock,LOCK_EX|LOCK_NB))throw new RuntimeException('BATCH_BUSY'); try{ $database=batchDatabase($directory); if($argv[1]==='export'){batchExport($database,$argv[2],$environment,$key,$argv[3]);$exit=0;} else{$rows=batchRows($argv[2]);batchConfiguration($environment);$exit=batchApply($rows,$database,$environment,$key);} }finally{flock($lock,LOCK_UN);fclose($lock);sodium_memzero($key);} exit($exit); }catch(Throwable $error){ // No raw exception, file path, key, response body or customer data in diagnostics. $code=preg_match('/^[A-Z_]+$/D',$error->getMessage())?$error->getMessage():'LOCAL_OPERATION_FAILED'; echo json_encode(['completed'=>false,'error'=>$code]).PHP_EOL;exit(1); } ``` ## Find a customer and help them 1. Select the correct environment and open Customers. 2. Locate the verified email in the displayed directory, using your browser's find [feature](/docs/v1/start/glossary#feature) if needed. 3. Check External ID, license count and the corresponding Licenses rows against your own order mapping. 4. If the license ID is known, use [get license](/docs/v1/api/get-license) from your authenticated backend for its scoped current summary. Expected result: you find the intended customer without an invented public email-search endpoint. The v1 GET operation requires a known license ID and does not return the complete key. Customers also provides Create link with a selected duration and an optional Email checkbox. The link allows the customer to inspect their licenses/[devices](/docs/v1/start/glossary#device) and release devices; it is not a full-key recovery link or a store-billing cancellation portal. Creating another link revokes the previous active customer link. Protect these bearer links like credentials and verify the recipient before generating one. ## Resend or replace a key If your private outbox has the original key, verify the customer's identity and resend that same saved key through your authorized delivery workflow. Do not call create again just to resend an email. Neither the dashboard mask, customer portal nor license GET response can reconstruct a lost plaintext key. If no authorized copy exists, investigate the original issuance outcome before deciding on a replacement. Replacing access is an explicit support operation: verify the buyer, document the mapping, issue a replacement only when authorized, deliver it privately and revoke the old license when appropriate. Revocation is terminal in the current lifecycle, so do not revoke a working license before planning the handover. The server's sensitive 24-hour idempotency response retention is not a public long-term key vault. ## Expected result Each approved order line has one durable issuance record and one privately delivered key. Reruns reuse your ledger, conflicting inputs stop and uncertain downstream results are investigated instead of blindly retried. ## Common mistakes - Importing the example placeholders without replacing product/plan references. - Deleting the ledger or changing namespace to force a duplicate batch through. - Treating an HTTP timeout or REVIEW state as proof that nothing was created. - Emailing a key mask, storing an outbox key beside the encrypted database or printing complete keys in logs. ## Security notes Manual issuance does not confirm a payment, refund or subscription. Keep provider secrets and developer API keys only on your backend. Encrypt key delivery state, restrict filesystem permissions and backups, keep an access-controlled audit record, and retain customer data only as required. A CSV is personal data even when its exported keys are encrypted separately. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | INVALID_CSV_HEADER or INVALID_CSV_ROW | Wrong header, encoding, repeated order ID or invalid value | Correct the whole CSV and rerun dry-run before applying | | DIRECTORY_MUST_BE_OWNER_ONLY | Private directory permits other Unix users | Secure a dedicated private directory; do not change a shared web root's permissions | | KEY_ENVIRONMENT_MISMATCH | Configured key prefix differs from selected environment | Use a properly scoped key from the intended environment | | REVIEW with PLAN_NOT_FOUND | Key/product/plan mapping is invalid | Correct the mapping and reconcile the held record; do not create another order reference to bypass it | | REVIEW after a timeout or 500 | Creation may already have committed | Inspect customer/license rows and saved request_id before any retry | | OUTBOX_DECRYPTION_FAILED | Wrong or lost private encryption key | Restore the correct secure backup; do not create a second license merely to obtain a key | ## Next steps Review [idempotency and uncertain outcomes](/docs/v1/api/idempotency), then test the recipient's [activation flow](/docs/v1/start/activation-validation-deactivation) using the delivered key. ## Related - [Create license API](/docs/v1/api/create-license) - [License lifecycle](/docs/v1/start/license-lifecycle) - [Developer data protection](/docs/v1/security/developer-data-protection) # Licensing recipes and supported limits Canonical: https://licensing.kdpika.com/docs/v1/guides/recipes --- title: "Licensing recipes and supported limits" description: "Fourteen tested licensing workflows, with explicit limits for plan migration, version gates, trials, device seats, expiry and key delivery." slug: "/docs/v1/guides/recipes" section: "Webhooks and integrations" order: 81 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/start/license-lifecycle","/docs/v1/guides/manual-issuance"] draft: false --- Choose a workflow that matches the current licensing contract instead of assuming a store [feature](/docs/v1/start/glossary#feature) is a licensing API feature. ## What you will learn - Configure supported trial, duration, installation and feature workflows. - Separate reversible suspension from terminal revocation. - Recognize unsupported configuration and migration operations before building automation. ## Prerequisites - Complete [the Sandbox quickstart](/docs/v1/start/quickstart). - Read [authentication](/docs/v1/start/authentication), [lifecycle](/docs/v1/start/license-lifecycle) and [idempotency](/docs/v1/api/idempotency). - Keep your own verified order-to-license mapping and buyer authorization on your backend. - Use the complete tested programs linked from each operation; do not embed a [developer](/docs/v1/start/glossary#developer) key in a buyer app. ## Support matrix | Recipe | Current support boundary | | --- | --- | | R01 Trial [licenses](/docs/v1/start/glossary#license) | Duration is supported; one-trial-per-person enforcement is your backend's responsibility | | R02 Perpetual and subscription licenses | Null or day-based expiration is supported; billing is your store's job | | R03 Upgrades | No in-place v1 plan change; use an explicit replacement/handover when authorized | | R04 Downgrades | No built-in scheduled license plan switch; schedule in your backend and handle replacement explicitly | | R05 Plan changes and renewal | Renewal adds days; it does not change plan, features or seat limits | | R06 Moving to a new [device](/docs/v1/start/glossary#device) | Deactivate or reset releases active device slots, not lifetime [activation count](/docs/v1/start/glossary#activation-count) | | R07 Multiple seats and teams | Per-license installation counts are supported; buyer team-user management is not | | R08 Application version ranges | Engine fields exist but public configuration is absent and omitted version skips the server check | | R09 Feature flags | Boolean plan feature creation and signed feature reads are supported; general numeric editing is not exposed | | R10 Grace after expiration | Offline transport grace does not extend license expiration | | R11 Bulk issuance | Use the tested CSV API tool, not an invented native importer | | R12 [Customer](/docs/v1/start/glossary#customer) self-service | Customer portal shows licenses/devices and permits own-device release; it is not a store-billing portal | | R13 Key email [delivery](/docs/v1/start/glossary#delivery) | Deliver your saved key privately; automatic key email/recovery is not provided | | R14 Chargebacks | Use an authenticated provider decision and explicit suspend/restore/revoke policy | ## R01 — Time-limited trials A [product plan](/docs/v1/start/glossary#product-plan) with billing type Trial uses trial_days when issuing a new license. A nonzero duration creates an expiration relative to issuance time, not first [activation](/docs/v1/start/glossary#activation). Trial creation uses the ordinary license-create API and its rate, [scope](/docs/v1/start/glossary#scope) and allowance rules. 1. Create a product plan with billing type Trial and a positive Trial days value. 2. Choose deliberate device and feature limits for evaluation. 3. Verify the requester using your own signup/order process. 4. Issue a Sandbox license and save its ID, complete key and expiry securely. 5. Activate it using the [full quickstart](/docs/v1/start/quickstart). 6. Test the application's expiration/refusal message before offering the trial publicly. Expected result: a seven-day trial receives an expiration about seven days after issuance. Do not assume a future starts_at delays access: it is not enforced as a not-before gate. Do not leave Trial days empty and assume a default evaluation period. Multiple creates for the same email can issue several trials; the platform does not provide one-trial-per-person, hardware anti-abuse or disposable-email enforcement for your [product](/docs/v1/start/glossary#product). Record trial eligibility on your own backend and do not rely on the Free [platform plan](/docs/v1/start/glossary#platform-plan) as the buyer's trial system. ## R02 — Perpetual versus subscription access Licensing duration and payment collection are separate. A plan label such as Monthly is not an automatic charge schedule. Creation derives expiry from trial_days for Trial plans or duration_days for other billing types. No duration means null expiration; selecting Lifetime with a populated Duration days does not automatically erase that duration. 1. For perpetual access, create the intended product plan with no duration and verify expires_at is null. 2. For a fixed paid period, configure the deliberate day count and verify the actual returned UTC expiration. 3. Record the verified store payment and resulting license ID in your own ledger. 4. For later paid periods, call [renew](/docs/v1/api/renew) only through a deduplicated, serialized workflow. 5. Reconcile the provider's actual calendar period with the day-based license expiration. Expected result: the perpetual fixture has no expiry and a 30-day fixture expires about 30 days after issuance. Thirty days is not every calendar month; 365 days is not every calendar year. The v1 renewal API cannot set an arbitrary exact timestamp. Do not silently round a store period or grant extra time without an explicit policy. A store subscription cancellation does not automatically mutate a buyer license. ## R03 — Upgrade by authorized replacement There is no public v1 endpoint or current license-dashboard control for changing an existing license's product plan. A safe supported alternative is a deliberate new license and handover, not a hidden database update or a call to renew disguised as an upgrade. 1. Verify the buyer's paid upgrade in your backend and confirm the intended target product plan. 2. Record one stable upgrade-operation ID before creating a replacement. 3. Create the target-plan license and save its complete key in your secured outbox. 4. Deliver the replacement key after authenticating the recipient. 5. Let the customer activate the replacement in your application's handover flow. 6. Reconcile the new license and explicitly revoke or suspend the old license according to your authorized policy. 7. Keep the old/new mapping for support and duplicate-event handling. Expected result: the replacement's signed features match its own target plan; the old license remains usable until you explicitly act on it. This is not an atomic migration API. Creating a replacement uses current plan defaults and starts its own duration; it does not automatically carry over expiry, installation history, discounts or payment proration. Revocation is terminal. Define and explain that handover before automating it, or handle the upgrade manually until your requirements fit the supported operations. ## R04 — Downgrade at the intended boundary The licensing API has no built-in scheduled plan-change resource. A store's scheduled downgrade or cancelled renewal must not be treated as an immediate instruction to revoke already-paid access. 1. Save the verified requested downgrade and its effective paid-through date in your backend. 2. Leave the current license untouched until your policy says the new access begins. 3. At the effective boundary, verify the current store/order state again. 4. Create and privately deliver a lower-plan replacement only when your policy authorizes it. 5. Reconcile that handover and retire the old license explicitly. 6. Check that the new signed payload no longer grants higher-plan-only features. Expected result: no premium feature is inferred from a buyer's historical plan name or a stale cache. The tested lower-plan fixture does not include pdf_export. The date scheduler, credit/refund calculation and paid-period reconciliation belong to your backend/store, not to a new KDPika endpoint. If precise seamless in-place migration is required, it is not supplied by the current v1 contract. ## R05 — Plan changes are not renewal [Renew](/docs/v1/api/renew) adds an integer from 1 through 3650 days. It extends a future expiration from that expiration; otherwise it extends from now. It can return an expired license to active, but a suspended license stays suspended. A revoked or cancelled license cannot renew. 1. Save each verified paid-period effect under a unique business-operation key. 2. Check the current license summary and expected old expiration. 3. Call renew once with the intended supported day count. 4. Persist the returned expiration and compare it with your recorded expectation. 5. If the response is lost, stop for reconciliation instead of adding the same days again. Expected result: the fixture adds exactly one day while plan_id remains unchanged. Renewal does not modify the product plan, feature schema, customer, max_devices or max_activations. Restoring a payment-suspended license is a separate authorized operation, and previously revoked [activation tokens](/docs/v1/start/glossary#activation-token) may require reactivation. Do not promise a public edit-plan or schedule-plan-change endpoint. ## R06 — Move a license to a new installation 1. Generate a stable random [installation ID](/docs/v1/security/installation-id) on each installation. 2. When the user can access the old installation, call [deactivate](/docs/v1/api/deactivate) there. 3. Confirm the old activation token is no longer usable. 4. Activate the key on the new installation with its own stable ID. 5. If the old installation is unavailable, authenticate the buyer before a support [device reset](/docs/v1/api/reset-devices). 6. Explain any lifetime activation limit before asking the user to try again. Expected result: deactivation or reset frees active device slots. Reset revokes active tokens and deactivates devices but does not clear the lifetime activation_count. A license can therefore have zero [active devices](/docs/v1/start/glossary#active-devices) and still return ACTIVATION_LIMIT_REACHED. Do not create a fresh random device ID on each application launch, clone a token between computers or promise unlimited transfers when max_activations is finite. ## R07 — Multiple seats without an invented team service A product plan's max_devices is the maximum active installation count for a license. It is not automatically a count of human users, concurrent sessions or verified employees. 1. Choose a supported positive Max devices value when creating the product plan. 2. State clearly whether your commercial seat policy means installations or named people. 3. For a shared installation-license arrangement, activate each permitted installation using a different stable ID. 4. Test the next installation beyond the maximum and handle DEVICE_LIMIT_REACHED clearly. 5. If you need named-user seats, authenticate and manage those memberships on your own backend or issue separately tracked licenses per authorized recipient. Expected result: a two-device fixture permits two active installations and refuses a third. The platform does not verify that two installations are two distinct humans. The developer's platform-plan team_members allowance is not a buyer-team invitation or seat-allocation API. Do not publish a [developer API key](/docs/v1/start/glossary#developer-api-key) or let arbitrary buyers manage someone else's devices. ## R08 — Version ranges are not a complete public workflow The engine can inspect stored min_version/max_version during activation when app_version is present. The signed payload also contains those fields. However, current v1 creation does not set those bounds and the dashboard has no supported version-range editor. Do not modify the production database from an integration guide to fill the gap. The isolated fixture test configures bounds only to verify existing engine behavior. It refuses 0.9.0 and 3.0.0 outside an example 1.0.0–2.0.0 range, and accepts 1.5.0. It also confirms a limitation: omitting app_version skips that server range check. Validation does not perform a new range check against a caller-supplied application version. 1. Do not advertise a configurable secure version-range product using the unsupported public workflow. 2. Always send your actual application version in supported clients. 3. Where version-specific feature access is needed, define stable feature keys in supported product plans and enforce them in a verified valid client result. 4. Keep valuable remote version-dependent actions authorized by your backend. 5. Confirm an approved configuration/enforcement workflow before relying on stored range fields. Expected result: the current limitation is explicit rather than represented as a security guarantee. A client-controlled version string is not proof of an untampered binary. This recipe supplies no fabricated set-version API, plan editor or automatic version entitlement migration. ## R09 — Feature-flag licensing 1. Choose stable lowercase feature keys when creating a product plan. 2. Issue a Sandbox license with the intended features. 3. Activate or validate it and verify the Ed25519 signature and identity/time policy. 4. Require a valid result before reading a feature. 5. Require an actual boolean true for a boolean feature and a validated integer for a count. 6. Test missing, false and wrong-typed values with restrictive defaults. Expected result: the Pro fixture's valid signed features include pdf_export=true; a lower fixture does not gain it. Use the [full guarded examples](/docs/v1/security/entitlements-and-features), not a truthy plan-name comparison. The current dashboard creates plans with boolean feature keys; it does not expose a general numeric JSON feature editor or a public feature-management API. An existing signed cache remains a snapshot until refreshed/refused. The current Plans & features screen offers creation and state/archive actions, not a general edit-plan form. ## R10 — Offline grace does not extend paid expiration [Offline grace](/docs/v1/start/glossary#offline-grace) is a transport-failure policy bounded by the signed deadline and license expiration. It is not grace days automatically added after subscription cancellation or a rejected payment. 1. Configure the supported product validation and offline-grace policy deliberately. 2. Test offline transport failure before the signed grace deadline while the license is still valid. 3. Test the exact signed expiration boundary and refuse access there. 4. Test a server rejection and confirm no offline permission is substituted. 5. For an explicit commercial courtesy extension, authorize a separate deduplicated renewal rather than modifying client time checks. Expected result: a signed grace deadline can be later than expires_at, yet expired access is still refused. Existing executed SDK tests deny the exact expiry boundary and permit only eligible transport fallback. See [offline mode and complete SDK configuration](/docs/v1/security/offline-mode). Renewing a past license extends from now, not from its old expiration; define the commercial effect before using it as a courtesy extension. ## R11 — Bulk license issuance 1. Verify the orders/recipients in your own source system. 2. Build a private one-product CSV with stable order-line IDs. 3. Run the [complete manual issuance tool's dry-run](/docs/v1/guides/manual-issuance#validate-without-issuing). 4. Apply it with a product/environment-scoped backend key and a private durable ledger. 5. Export saved keys only for authorized delivery. 6. Investigate held IN_FLIGHT, REVIEW or CONFLICT records before another write. Expected result: a repeated completed order does not create another license even after the API's replay window. The actual tool test loses a real successful response and confirms zero extra requests on rerun. No native CSV-import API is claimed. Protect the CSV as customer data and never email a developer API key with it. ## R12 — Customer self-service 1. Verify the customer and select the correct workspace environment. 2. Open Customers and find the intended recipient. 3. Choose the displayed portal-link duration and, when appropriate, Email. 4. Select Create link and deliver that private bearer URL only to its authorized recipient. 5. Let the customer review licenses/devices and release their own installation. 6. For a replacement link, create another and confirm the old link is no longer accepted. Expected result: the portal displays key masks, not original full keys, and can release a device owned by that customer. It does not offer buyer store refunds, change a store subscription, recover a lost plaintext key or create an owner account in your workspace. The exposed duration choices are one, four and 24 hours; server-side creation bounds the duration to 15–1440 minutes. These are bearer links: do not put them in analytics, public screenshots or a shared forum. ## R13 — License-key email delivery Creation returns the complete key to the authenticated issuing backend once; the outbound license.created event contains only its mask. LicenseService does not automatically send a complete-key email. Customer portal emails contain a portal link, not key recovery. 1. Save the successful creation result in your encrypted delivery outbox before marking the issuance complete. 2. Bind the intended recipient to your verified order/customer mapping. 3. Queue one delivery effect under a durable order-line/license delivery ID. 4. Send using your reviewed backend email workflow, with no API credential in the email. 5. On a resend request, authenticate the buyer and reuse that saved key rather than creating a new license. 6. If the original key is lost, investigate and plan an explicit authorized replacement instead of emailing the mask. Expected result: the issuance ledger/outbox separates license creation from email delivery. The [tested private export tool](/docs/v1/guides/manual-issuance#export-a-saved-key-for-delivery) makes the original key available without printing it to logs; email delivery itself is your backend workflow and is not falsely claimed as sent by that tool. Store keys encrypted with restricted access and a separate key backup. Do not make the platform's 24-hour sensitive replay response your permanent key vault. ## R14 — Refunds, disputes and chargebacks 1. Authenticate the actual provider event and match its account, environment and original order/license mapping. 2. Retrieve current provider state when needed; do not act on a browser return or an unverified reason string. 3. Decide whether the confirmed event requires no licensing action, temporary suspension or final revocation under your disclosed policy. 4. Use [suspend](/docs/v1/api/suspend) for a reversible investigation if appropriate. 5. After a confirmed resolution, [restore](/docs/v1/api/restore) only when the current license state and expiry permit it. 6. Use [revoke](/docs/v1/api/revoke) only for an explicitly authorized terminal access decision. 7. Persist the effect, outcome and original/current provider state in your durable ledger. Expected result: a suspended fixture can restore and reactivate; its revoked token does not become usable again automatically. A revoked license cannot be restored by calling restore, even if a dispute later closes in your favor. A partial refund, pending refund or newly opened dispute is not automatically proof that all paid access must end. The provider confirms money movement; the licensing API only changes license access. Renewal cancellation does not by itself refund a payment or invalidate already-paid access. ## Not supported as automatic v1 features | Requirement | Honest alternative or boundary | | --- | --- | | In-place/scheduled product-plan migration | Authorized replacement with explicit access/expiry handover, or obtain an approved product feature before promising it | | Exact calendar paid-through timestamp setter | Reconcile day-based supported renewal with your provider; do not silently invent a timestamp endpoint | | One trial per real human | Your backend eligibility/identity policy; repeated same-email creation is allowed | | Buyer-team invitations or named-user enforcement | Your own authenticated membership model or separately tracked individual licenses | | Version-range setup through v1/dashboard | No supported public setup; blank version skips the existing engine check | | General numeric feature editor | Validate typed reads, but confirm a supported configuration workflow before selling numeric limits | | Expired license access via offline grace | Not allowed; explicitly authorize any commercial extension | | Original-key recovery from mask or portal | Saved encrypted outbox, or an authorized replacement process | | Automatic provider refund/cancel/charge from licensing API | Your store/payment backend owns payment operations | ## Expected result Your implementation uses the actual lifecycle and tested examples, keeps payment confirmation on your backend and documents any unsupported commercial promise before offering it to buyers. ## Common mistakes - Calling renew to change a plan or repeating it after a lost response. - Calling a device count a verified human-seat count. - Assuming a trial label enforces one free trial per person. - Treating a signed notification or an SDK feature helper alone as permission to unlock access. ## Security notes Use [validity and feature guards](/docs/v1/security/entitlements-and-features) for every unlock. Public product IDs, plan names and a user-supplied version are not secrets or identity proofs. A user with full control of a local executable can patch local checks. Protect valuable online operations behind your own authenticated authorization, and handle refunds/access changes without exposing payment or developer credentials to the buyer application. ## Troubleshooting | Symptom | Actual boundary | Fix | | --- | --- | --- | | Renewed license still suspended | Renewal does not restore suspension | Reconcile payment/state and perform a separate authorized restore | | Reset license cannot activate | Lifetime activation count is exhausted | Review the supported transfer policy; reset is not a lifetime-counter reset | | Changed store plan still shows old license plan | No automatic product-plan migration exists | Use your recorded handover workflow, not renew | | An old feature remains visible | Client still has a prior signed snapshot | Validate online and inspect validity plus feature type | | Portal cannot show the full key | Only masks are available there | Use your private saved key or authorized replacement workflow | ## Next steps Compare the [four reference architectures](/docs/v1/guides/architectures), then implement the [manual issuance/reconciliation flow](/docs/v1/guides/manual-issuance) before automating store events. ## Related - [License lifecycle](/docs/v1/start/license-lifecycle) - [Idempotency and uncertain outcomes](/docs/v1/api/idempotency) - [Webhook security](/docs/v1/security/webhook-security) # Choose a licensing architecture Canonical: https://licensing.kdpika.com/docs/v1/guides/architectures --- title: "Choose a licensing architecture" description: "Trust boundaries, flows and trade-offs for desktop websites, SaaS backends, plugin marketplaces and CLI tools." slug: "/docs/v1/guides/architectures" section: "Webhooks and integrations" order: 82 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/recipes","/docs/v1/start/choose-your-path"] draft: false --- Choose where purchase confirmation, [license](/docs/v1/start/glossary#license) issuance and application authorization [live](/docs/v1/start/glossary#live) before copying an integration example. ## What you will learn - Separate buyer-facing clients from backend credentials and payment verification. - Choose a practical flow for desktop, SaaS, plugin or CLI software. - Understand offline, migration, seat and recovery trade-offs without inventing platform capabilities. ## Prerequisites - Read [core concepts](/docs/v1/start/core-concepts) and [authentication](/docs/v1/start/authentication). - Complete [one Sandbox quickstart](/docs/v1/start/quickstart). - Identify your store, trusted backend, buyer identity model and valuable protected actions. ## Shared trust boundaries | Boundary | Responsibility | Credential rule | | --- | --- | --- | | Store/provider | Confirms payment/refund/subscription state | Provider secrets stay on your backend | | Your fulfillment backend | Verifies incoming store data, maps purchases and issues/delivers licenses durably | [Product](/docs/v1/start/glossary#product)/environment-scoped lmp_ key; never distributed | | [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) | Manages license/[device](/docs/v1/start/glossary#device) lifecycle and signed responses | Public X-App-ID for [activation](/docs/v1/start/glossary#activation); installation act_ for validation | | Your buyer application | Verifies signed result and restrictively checks validity/[features](/docs/v1/start/glossary#feature) | [Public App ID](/docs/v1/start/glossary#public-app-id) and its own protected installation token, not [developer](/docs/v1/start/glossary#developer)/store keys | | Your valuable remote service | Authenticates caller and authorizes tenant/resource/action | Your application's session/service authorization plus current licensing policy | KDPika Licensing bills developers for their workspace. It does not become the merchant of record or process payments from your software buyers merely because you use a licensing API. Your store and fulfillment backend handle those transactions. Signed [webhooks](/docs/v1/start/glossary#webhook) are notifications, not access tokens or payment confirmations from a browser. ## Desktop application with your own website ![Desktop website flow: authenticate a store payment on your backend, issue once with a scoped key, deliver privately, then activate and verify in the installation.](/docs/diagrams/p8-architecture-desktop.svg) Desktop website flow: authenticate a store payment on your backend, issue once with a scoped key, deliver privately, then activate and verify in the installation. 1. The buyer purchases through your chosen store/payment provider. 2. Your website backend verifies the provider event or confirmed API state. 3. It maps the purchased item to one [product plan](/docs/v1/start/glossary#product-plan) and records a stable order-line effect before issuing. 4. It saves the returned complete key in an encrypted outbox and delivers it privately. 5. The desktop app activates with the public App ID, key, optional matching email and its stable [installation ID](/docs/v1/start/glossary#installation-id). 6. It verifies the signed response and uses a restrictive validity/feature guard. 7. Subsequent checks follow the signed validation/offline policy; a transfer releases or resets the prior device deliberately. Expected result: your client never has the [developer API key](/docs/v1/start/glossary#developer-api-key) and your website never relies on a success-page URL as payment proof. The complete [Python/Electron/C# sample applications](/docs/v1/sdks/sample-apps) demonstrate client behavior; [manual issuance](/docs/v1/guides/manual-issuance) demonstrates the supported backend creation/[delivery](/docs/v1/start/glossary#delivery) boundary. | Decision | Benefit | Trade-off | | --- | --- | --- | | Verified local cache with bounded offline policy | Temporary network loss need not stop valid access | Revocation/feature changes are not instantaneous in an offline client | | Email-matched activation | Optional check against the stored recipient email | Not proof of a human identity and not a substitute for verified purchase | | Backend-controlled high-value actions | Local patching cannot directly grant remote access | You operate authenticated server authorization and availability | | Encrypted key outbox | Authorized resend without issuing again | You must protect the encryption key, backups, recipients and retention | ## SaaS backend ![SaaS flow: authenticate the browser to your backend, reconcile verified store events with tenant/license records, and authorize protected server actions without exposing developer credentials.](/docs/diagrams/p8-architecture-saas.svg) SaaS flow: authenticate the browser to your backend, reconcile verified store events with tenant/license records, and authorize protected server actions without exposing developer credentials. 1. Authenticate the buyer using your own application login. 2. Resolve the caller's authorized tenant; do not accept an arbitrary browser-supplied license ID as ownership. 3. Process store events on your backend and keep the tenant/order/license relationship durably. 4. Use the backend developer key only for management operations scoped to that product/environment. 5. If your backend uses an installation activation/token workflow, define its stable service identity and protect that token like a secret. 6. Verify signed activation/validation results and enforce the valid feature/tenant decision before a protected action. 7. Reconcile refused/expired state without substituting a stale notification for authorization. Expected result: a browser session can call only resources its tenant owns; license metadata never becomes an unauthenticated bearer permission. The [PHP backend sample](/docs/v1/sdks/sample-apps) demonstrates a guarded server route. A license GET summary is an authenticated management response, not an Ed25519 activation envelope or the caller's application login. | Decision | Benefit | Trade-off | | --- | --- | --- | | Authorization on your backend | Central enforcement of valuable online operations | You must implement tenant/caller ownership and audit decisions | | Per-tenant service installation | Clear token/license mapping | Counts/identity must match your product's installation policy | | Cached licensing result | Lower latency and reduced API volume | Define bounded staleness; never turn an explicit refusal into fallback permission | | Separate commercial seat model | Can reflect named people and memberships | Platform device counts do not implement your SaaS user directory | ## Plugin or extension sold in a marketplace ![Plugin flow: verify marketplace ownership on a trusted fulfillment backend, deliver a license privately, and verify installation-bound results inside the plugin or host process.](/docs/diagrams/p8-architecture-plugin.svg) Plugin flow: verify marketplace ownership on a trusted fulfillment backend, deliver a license privately, and verify installation-bound results inside the plugin or host process. 1. Confirm how the marketplace exposes a verifiable completed purchase or ownership result. 2. Verify its notification/API response on your backend before mapping a product plan. 3. Store marketplace account, item/order-line and recipient context alongside the license ID. 4. Deliver the key through an authenticated private channel, not public extension configuration or a webpage source. 5. Activate the plugin using a stable installation ID appropriate to the host application. 6. Verify result signature, identity, expiry and feature types before allowing local or remote functionality. 7. Route transfer/refund/support actions through your own verified ownership workflow. Expected result: a copied public plugin package does not reveal a developer/provider key. A marketplace compatibility requirement is not automatically a KDPika feature: if the marketplace has no authenticated event mechanism, use its supported server API verification or a reviewed manual process rather than inventing an HMAC header. | Decision | Benefit | Trade-off | | --- | --- | --- | | Host installation identity | Works without collecting hardware serial numbers | Define how host reinstall/profile clones affect devices | | PHP/Node backend keeps management secret | Public plugin contains only client-safe identity and local token | Server-side plugin code shipped to a [customer](/docs/v1/start/glossary#customer) is still under that customer's control | | Optional own backend for premium content | Remote authorization can protect downloads/services | You operate that service and buyer authentication | | Domain/site labels | Helpful diagnostics | A site URL or domain string is not a built-in cryptographic domain-binding entitlement | Do not promise WordPress domain-locked licenses, named-site transfer quotas, marketplace payout processing or automatic update-distribution APIs from the present licensing contract. Confirm those separate requirements before offering them commercially. ## CLI tool and headless environments ![CLI flow: issue through a verified backend, deliver through a private user or CI channel, persist installation identity and check signed validity before executing a protected command.](/docs/diagrams/p8-architecture-cli.svg) CLI flow: issue through a verified backend, deliver through a private user or CI channel, persist installation identity and check signed validity before executing a protected command. 1. Issue the key from a verified purchase using your backend ledger. 2. Deliver it to the authorized user or a reviewed CI secret store. 3. Generate one stable random installation ID for the intended installation [scope](/docs/v1/start/glossary#scope). 4. Activate without printing keys or tokens to terminal output or CI logs. 5. Persist tokens using the [supported storage approach](/docs/v1/security/secure-token-storage). 6. Verify the signed result before running protected commands; return a clear nonzero refusal when access fails. 7. Release an installation when appropriate; do not assume reset clears lifetime activation limits. Expected result: the [Python/Go/Java CLI examples](/docs/v1/sdks/sample-apps) exercise activation, validation, feature gating, offline behavior, deactivation and error UX. Headless input/output is not a reason to distribute the developer API key. | Decision | Benefit | Trade-off | | --- | --- | --- | | Persistent identity on a workstation | Predictable device binding across runs | Protect the user's profile and recovery process | | Ephemeral CI runners | Disposable build execution | A new identity per runner can consume active/lifetime allowances; define a permitted runner policy first | | Separate license per CI installation | Clear accounting | More licenses/support records; do not share one [activation token](/docs/v1/start/glossary#activation-token) across machines | | Offline CLI operation | Works during eligible transport failures | Clock/cache integrity is local, not a trusted hardware guarantee | ## Choose before implementation | Question | Record this decision | | --- | --- | | Who confirms the purchase? | Your provider-authenticated backend; never the client success page | | Who owns a license? | Your verified customer/tenant/order mapping in the intended environment | | What counts as a device or seat? | Stable installation scope, with explicit distinction from named users/concurrency | | When does paid access end? | Actual stored expiry and signed policy, reconciled with the store period | | What happens after a timeout? | Durable held review and reconciliation, not automatic repeated create/renew | | How can the customer recover? | Authorized portal/device release or saved-key delivery/replacement process | | What remains valuable on a patched client? | Remote operations protected by your own server authorization | ## Expected result Your architecture has a clear purchase-verification boundary, backend-only management secrets, installation-safe credentials and a deliberate authorization/recovery policy. These diagrams describe supported relationships, not evidence of a completed marketplace app, live payment or production availability. ## Common mistakes - Using a developer API key in an Electron renderer, browser page, distributed plugin or CLI configuration. - Confusing a developer workspace subscription with the buyer's product plan. - Treating webhook notifications, key masks or an unsigned license summary as signed client authorization. - Assuming device counts implement named-user seats or a copied domain string proves domain ownership. ## Security notes TLS, verified signatures and encrypted local state are useful controls, not a promise that local software cannot be patched. Apply the [threat model](/docs/v1/security/threat-model), protect high-value remote actions and record only privacy-justified installation/customer metadata. Keep Sandbox and Live keys, mappings, recipients and endpoints separated. Never collect card/CVC data in the licensing backend to make any architecture work. ## Troubleshooting | Symptom | Design boundary | Action | | --- | --- | --- | | Browser needs a developer key | Management calls were placed on the wrong side of the trust boundary | Move them to your authenticated backend | | New CI job consumes another device | Installation identity is ephemeral | Define a supported persistent/separate-runner license policy | | Marketplace refund changes another customer | Order/license ownership mapping is incomplete | Verify provider/account/environment/order line before mutation | | SaaS request supplies another tenant's license ID | Resource ownership is not authorized | Resolve the caller's tenant on your backend and refuse cross-tenant access | | Old feature persists offline | Valid cached snapshot has bounded staleness | Follow the signed policy; use remote authorization when immediate control is necessary | ## Next steps Choose the [licensing recipes](/docs/v1/guides/recipes) that your architecture requires, then test [manual issuance and uncertain-result handling](/docs/v1/guides/manual-issuance) before adding store automation. ## Related - [Sample applications](/docs/v1/sdks/sample-apps) - [API authentication](/docs/v1/start/authentication) - [Signed response verification](/docs/v1/security/verify-ed25519) # Security and responsible disclosure Canonical: https://licensing.kdpika.com/security --- title: "Security and responsible disclosure" description: "Review KDPika Licensing's implemented controls, known limits and safe vulnerability-reporting contact." slug: "/security" section: "Security" order: 118 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/threat-model"] draft: false --- Report suspected vulnerabilities privately and evaluate KDPika's controls with their stated limitations. ## What you will learn - Understand implemented controls without unsupported guarantees. - Report an issue with a useful, redacted reproduction. - Know the boundaries of safe research and disclosure. ## Prerequisites - Use only your own test accounts, [products](/docs/v1/start/glossary#product) and [Sandbox](/docs/v1/start/glossary#sandbox) data. - Read [the threat model](/docs/v1/security/threat-model). - Do not test another tenant or real buyer data. ## Implemented controls | Area | Source-backed behavior | Limit | | --- | --- | --- | | Passwords | PHP password hashing, Argon2id when available with a configured fallback | No claim of independently audited authentication | | [Developer](/docs/v1/start/glossary#developer)/[activation](/docs/v1/start/glossary#activation) credentials | High-entropy credentials and hashed lookup records; scoped developer keys | A stolen active secret remains usable within its permissions until revoked | | Stored integration secrets | Sodium secretbox with application-key protection | Application-key/server compromise defeats that protection | | [License](/docs/v1/start/glossary#license) responses | Ed25519 signature over canonical payload, product/environment/installation checks in clients | Authenticity is not encryption or freshness; known SDK canonicalization limits are documented | | Tenant boundaries | Organization-scoped resource queries and permission checks | Source review is not a formal tenant-isolation audit | | [Webhooks](/docs/v1/start/glossary#webhook) | HMAC-SHA256 over timestamp and raw body; HTTPS target validation | Receivers must enforce tolerance and deduplication | | Operational records | API [request IDs](/docs/v1/start/glossary#request-id), metadata logs and audit events | Not an immutable external audit ledger | | Privacy tools | Workspace export and rights-request tracking | Account closure is not comprehensive automatic erasure | These statements describe code-backed controls, not a certification, penetration-test result, SLA or promise of complete protection. Current signature examples and [test vectors](/docs/v1/security/test-vectors) expose interoperability limits honestly rather than disabling checks. ## Report a vulnerability Send a private email to [hi@kdpika.com](mailto:hi@kdpika.com) with the subject Security report — [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing). Include: - A concise description and affected public URL or component. - A minimal reproduction using only your own synthetic Sandbox data. - Expected versus actual behavior and a practical impact description. - UTC time and redacted request IDs where useful. - Your preferred contact method. Do not include passwords, private signing keys, API secrets, [activation tokens](/docs/v1/start/glossary#activation-token), bank-card data, other users' information or a bulk data dump. If sensitive evidence is unavoidable, ask for an agreed secure transfer method first. No public PGP key or encrypted-reporting channel is currently published; do not assume ordinary email is end-to-end encrypted. ## Research boundaries Stop when you have the minimum evidence needed. Do not access other tenants, disrupt service, flood endpoints, exploit payment systems, test social engineering, install persistence or alter/delete data you do not own. Contact support before any test that could exceed these boundaries. This policy does not grant authorization for third-party provider testing. It does not promise a bounty, fixed response SLA or legal safe-harbor terms. Coordinate publication with the operator after reporting; do not disclose usable secrets. ## security.txt The public [security.txt file](/.well-known/security.txt) follows the contact/policy format in [RFC 9116](https://www.rfc-editor.org/rfc/rfc9116.html). It points to the same reporting address and expires 2027-04-15. Expiry is a maintenance reminder, not a vulnerability-response deadline. ## Operator and related notices KDPika is operated by KHALED AL HARAZI, 11 AVENUE ARISTIDE BRIAND, 69190 SAINT-FONS, France. SIREN 899816219; SIRET 89981621900013. Contact hi@kdpika.com. The operator's French address does not establish hosting geography. Review [Privacy](/legal/privacy), [DPA](/legal/dpa), [subprocessors](/legal/subprocessors), [legal notice](/legal/legal-notice) and [Status](/status). Unknown provider/contract/region facts are not represented as confirmed certifications or EU-only processing. ## Triage and recovery 1. Send the minimal private report. 2. Retain only your own necessary evidence securely. 3. Stop active testing if it risks other accounts or availability. 4. Coordinate clarification and any retest with the operator. 5. Remove disposable test artifacts when the investigation no longer needs them. Expected result: a useful report reaches the published contact without exposing [customers](/docs/v1/start/glossary#customer) or secrets. A response-time commitment is not claimed. ## Expected result The reporting contact and policy are publicly accessible without an account, and security claims remain bounded by evidence. ## Common mistakes - Posting a usable secret in a public issue or feedback form. - Demonstrating impact by accessing another customer's records. - Assuming a certification or bounty exists because a security page exists. ## Security notes Use the [API-key leak playbook](/docs/v1/security/api-key-leak-response) for your own compromised management key. Suspected platform signing-key or tenant-isolation issues should be escalated privately to the operator. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Report needs sensitive evidence | Ordinary email may be unsuitable | Ask for an agreed secure transfer method | | Issue concerns a provider | Different security boundary | Follow that provider's authorized reporting policy | | security.txt is expired | Contact metadata needs renewal | Contact hi@kdpika.com; do not infer service insecurity | ## 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) # Troubleshoot webhook notifications Canonical: https://licensing.kdpika.com/docs/v1/guides/webhook-troubleshooting --- title: "Troubleshoot webhook notifications" description: "Diagnose routing gaps, HMAC failures, stale requests, duplicates, delivery timeouts and exhausted retries using verified sender behavior." slug: "/docs/v1/guides/webhook-troubleshooting" section: "Webhooks and integrations" order: 146 audience: "all" level: "intermediate" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/test-webhooks-locally","/docs/v1/guides/webhook-delivery"] draft: false --- Find the failed boundary before retrying a notification or changing a [license](/docs/v1/start/glossary#license). ## What you will learn - Separate routing, authentication, persistence and business-processing failures. - Recognize documented gaps rather than relying on misleading retry assumptions. - Collect minimal evidence for support without disclosing credentials. ## Prerequisites - Access to your workspace [Webhooks](/docs/v1/start/glossary#webhook) page. - Private receiver logs containing event IDs and safe status codes, not raw secrets. - The [event catalog](/docs/v1/guides/webhook-events) and [delivery contract](/docs/v1/guides/webhook-delivery). ## Check the boundaries in order 1. Confirm the business operation actually emits the expected event. 2. Check [organization](/docs/v1/start/glossary#organization), environment, [product](/docs/v1/start/glossary#product), endpoint status and subscribed event. 3. Match the event ID in Delivery history to your receiver inbox. 4. Verify transport/HTTP status and attempt count. 5. Check authentication against the exact raw request bytes. 6. Check the durable inbox/work transaction. 7. Reconcile current API state before repeating a business action. Expected result: you identify the first failed boundary. Do not repeat create or renew just because an alert email or HTTP acknowledgement is missing. ## Routing and missing notifications | Symptom | Verified cause or boundary | Fix | | --- | --- | --- | | No history row for an ordinary event | Endpoint did not match active status, environment, product or subscription | Check setup before repeating the business operation | | No notification after [developer](/docs/v1/start/glossary#developer) [device](/docs/v1/start/glossary#device) release | That developer Devices action does not emit license.deactivated | Read current device/license state and use an audit workflow | | No notification for every validation | license.validated is not an event | Use API logs for diagnosis, not a nonexistent event subscription | | Events are missing across a disabled period | Ordinary publications do not target disabled endpoints | Reconcile your own records; enabling is not backfill | | Send test queues but does not send | Disabled endpoint, deferred processing or delivery failure | Enable, inspect status/count/next attempt and test again | | No reminder after another renewal | Reminder marker uses license and day count, not expiration revision | Schedule billing reminders in your backend and reconcile expiry | | No expiration notification immediately after API refusal | The persistent expired transition is performed by maintenance | Enforce the API/signed validity decision without waiting for notification | ## Signature and envelope failures | Symptom | Verified cause or boundary | Fix | | --- | --- | --- | | HMAC mismatch | Secret or raw bytes differ | Capture raw bytes before middleware and use the correct endpoint secret | | Signature fails after Rotate | New attempts immediately use the replacement secret | Coordinate receiver configuration and run Send test | | Old event passes freshness | Its delivery attempt was freshly signed | Check attempt timestamp, not event.created_at | | Future/stale signature refused | Receiver clock or request freshness is outside policy | Correct clock synchronization; regenerate only synthetic local requests | | Header ID/name differs from body | Untrusted or malformed envelope metadata | Refuse or quarantine; do not route by the header alone | | [Sandbox](/docs/v1/start/glossary#sandbox) notification reaches [Live](/docs/v1/start/glossary#live) handler | Receiver URL/configuration overlaps environments | Pin expected signed environment and separate endpoint secrets | | Same signature appears in two requests | Possible replay inside tolerance | Verify and deduplicate the authenticated event ID durably | ## Transport and processing failures | Symptom | Verified cause or boundary | Fix | | --- | --- | --- | | 301 or 302 in history | Sender does not follow redirects | Configure the final HTTPS URL | | No HTTP status, timeout error | Connection or total request timeout | Check TLS/DNS and acknowledge after fast durable persistence | | Receiver returns 429 | The sender still follows its fixed retry delays | Reduce work per request; do not depend on Retry-After being honored | | HTTP 200 but no durable work | Handler acknowledged before commit or lost its in-memory queue | Use a durable transactional inbox before sending 2xx | | Repeated emails or renewals | Authentication was mistaken for business deduplication | Use a separate durable order/invoice effect key | | Same event ID, different authenticated body | Conflicting reuse of an ID | Quarantine and investigate; do not apply a second action | | Older state overwrites newer state | Delivery order is not guaranteed | Reconcile current scoped API state before updating your view | | Key email contains a mask | Webhook payloads never contain the complete [license key](/docs/v1/start/glossary#license-key) | Use your secured issuance outbox; do not reconstruct the key | ## Retry and history limitations | Symptom | Verified cause or boundary | Fix | | --- | --- | --- | | Retry does nothing at attempt 6 | Count is not reset; exhausted delivery is still guarded | Reconcile affected resource and contact support for investigation | | Delivered notification cannot replay | Sender skips Delivered rows | Test locally with a synthetic fixture; do not repeat a real business effect | | Next attempt is later than the listed delay | Due work waits for maintenance processing | Inspect next attempt and receiver health; the delay is a minimum | | An old Delivered row is absent | Latest-250 view or 30-day delivered cleanup | Use your own minimal durable event/business ledger | | Response body is truncated | Only a capped excerpt is retained | Log a safe correlation ID, never credentials or complete keys | ## Prepare a support request 1. Record the environment, affected endpoint host and approximate UTC time. 2. Include the authenticated event ID and dashboard attempt count/status. 3. Describe the expected event and the operation that triggered it. 4. Include only safe HTTP/error codes and your own correlation ID. 5. Contact hi@kdpika.com without attaching keys, signatures, complete payloads or [customer](/docs/v1/start/glossary#customer) payment details. Expected result: support can identify the delivery without obtaining a secret. Do not ask a buyer for card details or paste a [developer API key](/docs/v1/start/glossary#developer-api-key) into a ticket. ## Expected result You can distinguish a missed notification from an uncertain licensing operation and avoid compounding a failure with a duplicate mutation. ## Common mistakes - Repeatedly pressing Retry after the attempt budget is exhausted. - Repeating renew to compensate for a missing webhook. - Assuming Delivered proves the receiver's business task finished. - Treating a stale status in an event as the current authoritative license state. ## Security notes Keep signature rejection responses generic. Never turn off authentication to make a callback succeed. A correct support/reconciliation workflow does not require changing the licensing API contract or sharing private keys. ## Troubleshooting If none of the symptom tables identifies the issue, reproduce it with the [local synthetic inbox](/docs/v1/guides/test-webhooks-locally). Compare raw-byte verification, durable receipt and business processing independently before escalating. ## Next steps Run [local signature and replay tests](/docs/v1/guides/test-webhooks-locally), then retest only the eligible failed delivery after fixing its cause. ## Related - [Endpoint setup and rotation](/docs/v1/guides/webhooks-overview) - [Idempotency limitations](/docs/v1/api/idempotency) - [API key leak response](/docs/v1/security/api-key-leak-response) # Python signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/python --- title: "Python signature verification" description: "Run the complete Python Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/python" section: "Signature examples" order: 201 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Python without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Python verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Python 3.9+; cryptography 46.0.3. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash python3 -m venv .venv . .venv/bin/activate python -m pip install cryptography==46.0.3 ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash python python/verify.py vectors/positive.key.json vectors/positive.envelope.json python python/verify.py vectors/tampered.key.json vectors/tampered.envelope.json python python/verify.py vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command python python/verify.py ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```python """Verify a raw KDPika envelope, not permission to use a license. Python 3.9+, cryptography==46.0.3. Run: python verify.py key.json envelope.json Output: signature_valid=true (exit 0) or signature_valid=false (exit 1). Keep key.json separately trusted; never take it from the untrusted envelope. """ import base64 import hashlib import json import re import sys from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey class Number(str): """Retain the original server JSON numeric token, including 19.0.""" def pairs(items): result = {} for key, value in items: if key in result: raise ValueError('Duplicate JSON property') result[key] = value return result def parse(raw): if len(raw) > 1048576: raise ValueError('JSON exceeds 1 MiB') return json.loads(raw.decode('utf-8'), parse_int=Number, parse_float=Number, parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value)), object_pairs_hook=pairs) def quote(value): value.encode('utf-8', errors='strict') return json.dumps(value, ensure_ascii=False).replace('\u2028', '\\u2028').replace('\u2029', '\\u2029') def canonical(value, depth=0): if depth > 64: raise ValueError('JSON exceeds 64 levels') if isinstance(value, Number): return str(value) if isinstance(value, dict): return '{' + ','.join(quote(k) + ':' + canonical(value[k], depth + 1) for k in sorted(value, key=lambda k: k.encode('utf-8'))) + '}' if isinstance(value, list): return '[' + ','.join(canonical(v, depth + 1) for v in value) + ']' if isinstance(value, str): return quote(value) if value is None: return 'null' if isinstance(value, bool): return 'true' if value else 'false' raise ValueError('Unsupported JSON value') def decode(value, size): if not isinstance(value, str) or not re.fullmatch(r'[A-Za-z0-9_-]+', value): raise ValueError('Invalid base64url') raw = base64.urlsafe_b64decode(value + '=' * (-len(value) % 4)) if len(raw) != size or base64.urlsafe_b64encode(raw).decode().rstrip('=') != value: raise ValueError('Invalid base64url size or padding bits') return raw def verify_envelope(trusted_key_raw, envelope_raw): """False on malformed data or signature failure; does not check license policy.""" try: key, env = parse(trusted_key_raw), parse(envelope_raw) public = decode(key['public_key'], 32) key_id = 'ed25519_' + hashlib.sha256(public).hexdigest()[:16] if (key['algorithm'] != 'Ed25519' or key['encoding'] != 'base64url' or key['key_id'] != key_id or env['algorithm'] != 'Ed25519' or env['key_id'] != key_id or not isinstance(env['payload'], dict)): return False Ed25519PublicKey.from_public_bytes(public).verify( decode(env['signature'], 64), canonical(env['payload']).encode('utf-8')) return True except (Exception, RecursionError): return False if __name__ == '__main__': try: if len(sys.argv) != 3: raise ValueError('Expected trusted key and envelope paths') valid = verify_envelope(Path(sys.argv[1]).read_bytes(), Path(sys.argv[2]).read_bytes()) except Exception: valid = False print('signature_valid=' + str(valid).lower()) sys.exit(0 if valid else 1) ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # PHP signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/php --- title: "PHP signature verification" description: "Run the complete PHP Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/php" section: "Signature examples" order: 202 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in PHP without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete PHP verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install PHP 8.2+ with ext-sodium; no Composer dependency. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash php -r 'exit(extension_loaded("sodium") ? 0 : 1);' ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash php php/verify.php vectors/positive.key.json vectors/positive.envelope.json php php/verify.php vectors/tampered.key.json vectors/tampered.envelope.json php php/verify.php vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command php php/verify.php ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```php 1048576) throw new RuntimeException('JSON exceeds 1 MiB'); } private function ws(): void { $this->i += strspn($this->text, " \t\r\n", $this->i); } private function string(): string { if (!preg_match('/\G"(?:[^"\\\\\x00-\x1f]|\\\\(?:["\\\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/s', $this->text, $m, 0, $this->i)) throw new RuntimeException('Invalid string'); $this->i += strlen($m[0]); return json_decode($m[0], false, 64, JSON_THROW_ON_ERROR); } private function value(int $depth): mixed { if ($depth > 64) throw new RuntimeException('JSON exceeds 64 levels'); $this->ws(); $ch = $this->text[$this->i] ?? ''; if ($ch === '"') return $this->string(); if ($ch === '{' || $ch === '[') { $object = $ch === '{'; $end = $object ? '}' : ']'; $this->i++; $this->ws(); $result = $object ? new stdClass() : []; if (($this->text[$this->i] ?? '') === $end) { $this->i++; return $result; } while (true) { if ($object) { $this->ws(); $key = $this->string(); $this->ws(); if (($this->text[$this->i++] ?? '') !== ':' || property_exists($result, $key)) throw new RuntimeException('Duplicate or invalid property'); $result->{$key} = $this->value($depth + 1); } else { $result[] = $this->value($depth + 1); } $this->ws(); $next = $this->text[$this->i++] ?? ''; if ($next === $end) return $result; if ($next !== ',') throw new RuntimeException('Invalid collection'); } } foreach (['true' => true, 'false' => false, 'null' => null] as $token => $value) { if (substr($this->text, $this->i, strlen($token)) === $token) { $this->i += strlen($token); return $value; } } if (!preg_match('/\G-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/', $this->text, $m, 0, $this->i)) throw new RuntimeException('Invalid value'); $this->i += strlen($m[0]); return new NumberToken($m[0]); } public function parse(): mixed { $value = $this->value(0); $this->ws(); if ($this->i !== strlen($this->text)) throw new RuntimeException('Trailing content'); return $value; } } function canonical(mixed $value): string { if ($value instanceof NumberToken) return $value->raw; if ($value instanceof stdClass) { $props = get_object_vars($value); ksort($props, SORT_STRING); $parts = []; foreach ($props as $key => $child) $parts[] = canonical((string)$key) . ':' . canonical($child); return '{' . implode(',', $parts) . '}'; } if (is_array($value)) return '[' . implode(',', array_map('canonical', $value)) . ']'; return json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); } function decodeKey(string $value, int $size): string { if (!preg_match('/^[A-Za-z0-9_-]+$/D', $value)) throw new RuntimeException('Invalid base64url'); $raw = base64_decode(strtr($value, '-_', '+/'), true); if ($raw === false || strlen($raw) !== $size || rtrim(strtr(base64_encode($raw), '+/', '-_'), '=') !== $value) throw new RuntimeException('Invalid base64url length'); return $raw; } function verifyEnvelope(string $trustedKeyRaw, string $envelopeRaw): bool { try { $key = (new RawJson($trustedKeyRaw))->parse(); $env = (new RawJson($envelopeRaw))->parse(); if (!($key instanceof stdClass) || !($env instanceof stdClass)) return false; foreach (['public_key','algorithm','encoding','key_id'] as $field) if (!isset($key->{$field}) || !is_string($key->{$field})) return false; foreach (['algorithm','key_id','signature','payload'] as $field) if (!isset($env->{$field})) return false; $public = decodeKey($key->public_key, 32); $id = 'ed25519_' . substr(hash('sha256', $public), 0, 16); if ($key->algorithm !== 'Ed25519' || $key->encoding !== 'base64url' || $key->key_id !== $id || $env->algorithm !== 'Ed25519' || $env->key_id !== $id || !($env->payload instanceof stdClass)) return false; return sodium_crypto_sign_verify_detached(decodeKey($env->signature, 64), canonical($env->payload), $public); } catch (Throwable) { return false; } } if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { $valid = false; try { if ($argc === 3) $valid = verifyEnvelope((string)file_get_contents($argv[1]), (string)file_get_contents($argv[2])); } catch (Throwable) { $valid = false; } echo 'signature_valid=' . ($valid ? 'true' : 'false') . PHP_EOL; exit($valid ? 0 : 1); } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # JavaScript signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/javascript --- title: "JavaScript signature verification" description: "Run the complete JavaScript Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/javascript" section: "Signature examples" order: 203 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in JavaScript without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete JavaScript verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Node.js 22+; built-in crypto; no npm dependency. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash node --version ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash node javascript/verify.mjs vectors/positive.key.json vectors/positive.envelope.json node javascript/verify.mjs vectors/tampered.key.json vectors/tampered.envelope.json node javascript/verify.mjs vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command node javascript/verify.mjs ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```javascript // Lossless KDPika JSON canonicalization; Node 22+. Used by JS and TS samples. // Import parseRaw and canonical. Numeric tokens remain in their server form. // Throws on malformed JSON, duplicate properties, invalid Unicode or depth >64. export class NumberToken { constructor(raw) { this.raw = raw; } } export function quote(value) { for (const character of value) { const cp = character.codePointAt(0); if (cp >= 0xd800 && cp <= 0xdfff) throw new Error('Invalid Unicode'); } return JSON.stringify(value).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } export function parseRaw(bytes) { if (bytes.length > 1048576) throw new Error('JSON exceeds 1 MiB'); const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); let pos = 0; const ws = () => { while (/[\x20\t\r\n]/.test(text[pos] || 'x')) pos++; }; function string() { const match = /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/.exec(text.slice(pos)); if (!match) throw new Error('Invalid JSON string'); pos += match[0].length; const value = JSON.parse(match[0]); quote(value); return value; } function value(depth) { if (depth > 64) throw new Error('JSON exceeds 64 levels'); ws(); const ch = text[pos]; if (ch === '"') return string(); if (ch === '{') { pos++; ws(); const result = Object.create(null); if (text[pos] === '}') { pos++; return result; } while (true) { ws(); const key = string(); ws(); if (text[pos++] !== ':' || Object.hasOwn(result, key)) throw new Error('Duplicate or invalid property'); result[key] = value(depth + 1); ws(); const next = text[pos++]; if (next === '}') return result; if (next !== ',') throw new Error('Invalid object'); } } if (ch === '[') { pos++; ws(); const result = []; if (text[pos] === ']') { pos++; return result; } while (true) { result.push(value(depth + 1)); ws(); const next = text[pos++]; if (next === ']') return result; if (next !== ',') throw new Error('Invalid array'); } } for (const [token, result] of [['true', true], ['false', false], ['null', null]]) { if (text.startsWith(token, pos)) { pos += token.length; return result; } } const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(text.slice(pos)); if (!number) throw new Error('Invalid JSON value'); pos += number[0].length; return new NumberToken(number[0]); } const result = value(0); ws(); if (pos !== text.length) throw new Error('Trailing JSON content'); return result; } export function canonical(value) { if (value instanceof NumberToken) return value.raw; if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']'; if (value !== null && typeof value === 'object') { const keys = Object.keys(value).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))); return '{' + keys.map(key => quote(key) + ':' + canonical(value[key])).join(',') + '}'; } return typeof value === 'string' ? quote(value) : JSON.stringify(value); } ``` ```javascript // KDPika signature authenticity only. Node 22+, built-in crypto, no npm package. // Run: node verify.mjs key.json envelope.json. Output signature_valid=true/false. // Pin key.json independently. Enforce license policy separately before access. import { createHash, createPublicKey, verify } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { parseRaw, canonical } from './canonical.mjs'; function decode(value, size) { if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid base64url'); const bytes = Buffer.from(value, 'base64url'); if (bytes.length !== size || bytes.toString('base64url') !== value) throw new Error('Invalid base64url length'); return bytes; } export function verifyEnvelope(trustedKeyRaw, envelopeRaw) { try { const key = parseRaw(trustedKeyRaw), env = parseRaw(envelopeRaw); const publicBytes = decode(key.public_key, 32); const id = 'ed25519_' + createHash('sha256').update(publicBytes).digest('hex').slice(0, 16); if (key.algorithm !== 'Ed25519' || key.encoding !== 'base64url' || key.key_id !== id || env.algorithm !== 'Ed25519' || env.key_id !== id || !env.payload || Array.isArray(env.payload) || typeof env.payload !== 'object') return false; const publicKey = createPublicKey({ key: Buffer.concat([ Buffer.from('302a300506032b6570032100', 'hex'), publicBytes ]), format: 'der', type: 'spki' }); return verify(null, Buffer.from(canonical(env.payload)), publicKey, decode(env.signature, 64)); } catch { return false; } } if (process.argv[1] === fileURLToPath(import.meta.url)) { let valid = false; try { if (process.argv.length === 4) valid = verifyEnvelope(readFileSync(process.argv[2]), readFileSync(process.argv[3])); } catch { valid = false; } console.log('signature_valid=' + valid); process.exitCode = valid ? 0 : 1; } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # TypeScript signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/typescript --- title: "TypeScript signature verification" description: "Run the complete TypeScript Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/typescript" section: "Signature examples" order: 204 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in TypeScript without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete TypeScript verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Node.js 22+; TypeScript 7.0.2; @types/node 22.19.7. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash npm install --no-audit --no-fund typescript@7.0.2 @types/node@22.19.7 npx tsc --allowJs --checkJs false --target es2022 --module nodenext --moduleResolution nodenext --types node --outDir build typescript/verify.ts ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash node build/typescript/verify.js vectors/positive.key.json vectors/positive.envelope.json node build/typescript/verify.js vectors/tampered.key.json vectors/tampered.envelope.json node build/typescript/verify.js vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command node build/typescript/verify.js ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```javascript // Lossless KDPika JSON canonicalization; Node 22+. Used by JS and TS samples. // Import parseRaw and canonical. Numeric tokens remain in their server form. // Throws on malformed JSON, duplicate properties, invalid Unicode or depth >64. export class NumberToken { constructor(raw) { this.raw = raw; } } export function quote(value) { for (const character of value) { const cp = character.codePointAt(0); if (cp >= 0xd800 && cp <= 0xdfff) throw new Error('Invalid Unicode'); } return JSON.stringify(value).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); } export function parseRaw(bytes) { if (bytes.length > 1048576) throw new Error('JSON exceeds 1 MiB'); const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes); let pos = 0; const ws = () => { while (/[\x20\t\r\n]/.test(text[pos] || 'x')) pos++; }; function string() { const match = /^"(?:[^"\\\u0000-\u001f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"/.exec(text.slice(pos)); if (!match) throw new Error('Invalid JSON string'); pos += match[0].length; const value = JSON.parse(match[0]); quote(value); return value; } function value(depth) { if (depth > 64) throw new Error('JSON exceeds 64 levels'); ws(); const ch = text[pos]; if (ch === '"') return string(); if (ch === '{') { pos++; ws(); const result = Object.create(null); if (text[pos] === '}') { pos++; return result; } while (true) { ws(); const key = string(); ws(); if (text[pos++] !== ':' || Object.hasOwn(result, key)) throw new Error('Duplicate or invalid property'); result[key] = value(depth + 1); ws(); const next = text[pos++]; if (next === '}') return result; if (next !== ',') throw new Error('Invalid object'); } } if (ch === '[') { pos++; ws(); const result = []; if (text[pos] === ']') { pos++; return result; } while (true) { result.push(value(depth + 1)); ws(); const next = text[pos++]; if (next === ']') return result; if (next !== ',') throw new Error('Invalid array'); } } for (const [token, result] of [['true', true], ['false', false], ['null', null]]) { if (text.startsWith(token, pos)) { pos += token.length; return result; } } const number = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/.exec(text.slice(pos)); if (!number) throw new Error('Invalid JSON value'); pos += number[0].length; return new NumberToken(number[0]); } const result = value(0); ws(); if (pos !== text.length) throw new Error('Trailing JSON content'); return result; } export function canonical(value) { if (value instanceof NumberToken) return value.raw; if (Array.isArray(value)) return '[' + value.map(canonical).join(',') + ']'; if (value !== null && typeof value === 'object') { const keys = Object.keys(value).sort((a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))); return '{' + keys.map(key => quote(key) + ':' + canonical(value[key])).join(',') + '}'; } return typeof value === 'string' ? quote(value) : JSON.stringify(value); } ``` ```javascript // KDPika signature authenticity only. Node 22+, built-in crypto, no npm package. // Run: node verify.mjs key.json envelope.json. Output signature_valid=true/false. // Pin key.json independently. Enforce license policy separately before access. import { createHash, createPublicKey, verify } from 'node:crypto'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { parseRaw, canonical } from './canonical.mjs'; function decode(value, size) { if (typeof value !== 'string' || !/^[A-Za-z0-9_-]+$/.test(value)) throw new Error('Invalid base64url'); const bytes = Buffer.from(value, 'base64url'); if (bytes.length !== size || bytes.toString('base64url') !== value) throw new Error('Invalid base64url length'); return bytes; } export function verifyEnvelope(trustedKeyRaw, envelopeRaw) { try { const key = parseRaw(trustedKeyRaw), env = parseRaw(envelopeRaw); const publicBytes = decode(key.public_key, 32); const id = 'ed25519_' + createHash('sha256').update(publicBytes).digest('hex').slice(0, 16); if (key.algorithm !== 'Ed25519' || key.encoding !== 'base64url' || key.key_id !== id || env.algorithm !== 'Ed25519' || env.key_id !== id || !env.payload || Array.isArray(env.payload) || typeof env.payload !== 'object') return false; const publicKey = createPublicKey({ key: Buffer.concat([ Buffer.from('302a300506032b6570032100', 'hex'), publicBytes ]), format: 'der', type: 'spki' }); return verify(null, Buffer.from(canonical(env.payload)), publicKey, decode(env.signature, 64)); } catch { return false; } } if (process.argv[1] === fileURLToPath(import.meta.url)) { let valid = false; try { if (process.argv.length === 4) valid = verifyEnvelope(readFileSync(process.argv[2]), readFileSync(process.argv[3])); } catch { valid = false; } console.log('signature_valid=' + valid); process.exitCode = valid ? 0 : 1; } ``` ```typescript // Typed KDPika verification entry point. Node 22+, TypeScript 7.0.2. // Uses the complete adjacent JS verifier and lossless canonicalization module. // Run compiled verify.js with key.json envelope.json; signature_valid=true/false. import { readFileSync } from 'node:fs'; import { verifyEnvelope } from '../javascript/verify.mjs'; export function verifyLicenseSignature(key: Uint8Array, envelope: Uint8Array): boolean { return verifyEnvelope(key, envelope); } let valid = false; try { if (process.argv.length === 4) valid = verifyLicenseSignature(readFileSync(process.argv[2]), readFileSync(process.argv[3])); } catch { valid = false; } console.log('signature_valid=' + valid); process.exitCode = valid ? 0 : 1; ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # C# signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/csharp --- title: "C# signature verification" description: "Run the complete C# Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/csharp" section: "Signature examples" order: 205 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in C# without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete C# verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install .NET 7+; BouncyCastle.Cryptography 2.7.0. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash dotnet build csharp/Verifier.csproj -o build/csharp --nologo ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash dotnet build/csharp/Verifier.dll vectors/positive.key.json vectors/positive.envelope.json dotnet build/csharp/Verifier.dll vectors/tampered.key.json vectors/tampered.envelope.json dotnet build/csharp/Verifier.dll vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command dotnet build/csharp/Verifier.dll ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```xml Exe net7.0 enable enable ``` ```csharp // KDPika signature authenticity only. .NET 7+, BouncyCastle.Cryptography 2.7.0. // Run: dotnet run --project Verifier.csproj -- key.json envelope.json // Output signature_valid=true/false. Independently trust the key; check policy next. using System.Security.Cryptography; using System.Text; using System.Text.Json; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Signers; public static class Verifier { private static readonly UTF8Encoding Utf8 = new(false, true); private static string Quote(string value) { Utf8.GetBytes(value); // Reject unpaired surrogates instead of replacing them. var output = new StringBuilder("\""); foreach (var rune in value.EnumerateRunes()) { output.Append(rune.Value switch { 34 => "\\\"", 92 => "\\\\", 8 => "\\b", 9 => "\\t", 10 => "\\n", 12 => "\\f", 13 => "\\r", < 32 => "\\u" + rune.Value.ToString("x4"), 0x2028 => "\\u2028", 0x2029 => "\\u2029", _ => rune.ToString() }); } return output.Append('"').ToString(); } private static int CompareUtf8(string a, string b) => Utf8.GetBytes(a).AsSpan().SequenceCompareTo(Utf8.GetBytes(b)); private static string Canonical(JsonElement value) { switch (value.ValueKind) { case JsonValueKind.Object: var properties = value.EnumerateObject().ToArray(); if (properties.Select(p => p.Name).Distinct(StringComparer.Ordinal).Count() != properties.Length) throw new FormatException("Duplicate JSON property"); Array.Sort(properties, (a, b) => CompareUtf8(a.Name, b.Name)); return "{" + string.Join(",", properties.Select(p => Quote(p.Name) + ":" + Canonical(p.Value))) + "}"; case JsonValueKind.Array: return "[" + string.Join(",", value.EnumerateArray().Select(Canonical)) + "]"; case JsonValueKind.String: return Quote(value.GetString()!); case JsonValueKind.Number: return value.GetRawText(); case JsonValueKind.True: return "true"; case JsonValueKind.False: return "false"; case JsonValueKind.Null: return "null"; default: throw new FormatException("Invalid JSON value"); } } private static byte[] Decode(string value, int size) { if (value.Length == 0 || value.Any(c => !(char.IsAsciiLetterOrDigit(c) || c is '-' or '_'))) throw new FormatException("Invalid base64url"); var result = Convert.FromBase64String(value.Replace('-', '+').Replace('_', '/') + new string('=', (4 - value.Length % 4) % 4)); if (result.Length != size || Convert.ToBase64String(result).TrimEnd('=').Replace('+', '-').Replace('/', '_') != value) throw new FormatException("Invalid base64url length"); return result; } private static JsonDocument Parse(byte[] raw) { if (raw.Length > 1048576) throw new FormatException("JSON exceeds 1 MiB"); var doc = JsonDocument.Parse(Utf8.GetString(raw), new JsonDocumentOptions { MaxDepth = 64 }); Canonical(doc.RootElement); // Validate duplicates and strings throughout the envelope. return doc; } public static bool VerifyEnvelope(byte[] trustedKeyRaw, byte[] envelopeRaw) { try { using var keyDoc = Parse(trustedKeyRaw); using var envDoc = Parse(envelopeRaw); var key = keyDoc.RootElement; var env = envDoc.RootElement; var bytes = Decode(key.GetProperty("public_key").GetString()!, 32); var id = "ed25519_" + Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant()[..16]; if (key.GetProperty("algorithm").GetString() != "Ed25519" || key.GetProperty("encoding").GetString() != "base64url" || key.GetProperty("key_id").GetString() != id || env.GetProperty("algorithm").GetString() != "Ed25519" || env.GetProperty("key_id").GetString() != id || env.GetProperty("payload").ValueKind != JsonValueKind.Object) return false; var message = Utf8.GetBytes(Canonical(env.GetProperty("payload"))); var verifier = new Ed25519Signer(); verifier.Init(false, new Ed25519PublicKeyParameters(bytes, 0)); verifier.BlockUpdate(message, 0, message.Length); return verifier.VerifySignature(Decode(env.GetProperty("signature").GetString()!, 64)); } catch { return false; } } public static int Main(string[] args) { var valid = false; try { if (args.Length == 2) valid = VerifyEnvelope(File.ReadAllBytes(args[0]), File.ReadAllBytes(args[1])); } catch { valid = false; } Console.WriteLine("signature_valid=" + valid.ToString().ToLowerInvariant()); return valid ? 0 : 1; } } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Java signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/java --- title: "Java signature verification" description: "Run the complete Java Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/java" section: "Signature examples" order: 206 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Java without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Java verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install JDK 17+; Gson 2.14.0; Bouncy Castle 1.86. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p dependencies build/java curl -fSL https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar -o dependencies/gson.jar curl -fSL https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.86/bcprov-jdk18on-1.86.jar -o dependencies/bcprov.jar javac -cp 'dependencies/gson.jar:dependencies/bcprov.jar' -d build/java java/Canonical.java java/Verify.java ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The commands use the macOS/Linux classpath separator, a colon. On Windows replace classpath separators with semicolons and quote the complete classpath. Source code is unchanged. JDK21 was used in the recorded test; the source targets Java17. ## Run the positive and negative cases ```bash java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/positive.key.json vectors/positive.envelope.json java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/tampered.key.json vectors/tampered.envelope.json java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command java -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' Verify ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```java // Lossless KDPika JSON tree. Java 17+, Gson 2.14.0. Shared with Kotlin verifier. // JsonReader retains numeric tokens; duplicate properties and bad Unicode fail. import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.*; public final class Canonical { private record NumberToken(String raw) {} private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); public static String quote(String value) { for (int i=0;i=value.length() || !Character.isLowSurrogate(value.charAt(i))) throw new IllegalArgumentException("Invalid Unicode"); } else if (Character.isLowSurrogate(c)) throw new IllegalArgumentException("Invalid Unicode"); } return GSON.toJson(value); } private static Object value(JsonReader reader,int depth) throws Exception { if (depth>64) throw new IllegalArgumentException("JSON exceeds 64 levels"); return switch(reader.peek()) { case BEGIN_OBJECT -> { reader.beginObject(); var map=new HashMap(); while(reader.hasNext()) { var key=reader.nextName(); quote(key); if(map.containsKey(key)) throw new IllegalArgumentException("Duplicate JSON property"); map.put(key,value(reader,depth+1)); } reader.endObject(); yield map; } case BEGIN_ARRAY -> { reader.beginArray(); var list=new ArrayList(); while(reader.hasNext()) list.add(value(reader,depth+1)); reader.endArray(); yield list; } case STRING -> { var text=reader.nextString(); quote(text); yield text; } case NUMBER -> new NumberToken(reader.nextString()); case BOOLEAN -> reader.nextBoolean(); case NULL -> { reader.nextNull(); yield null; } default -> throw new IllegalArgumentException("Invalid JSON value"); }; } public static Object parse(byte[] raw) throws Exception { if(raw.length>1048576) throw new IllegalArgumentException("JSON exceeds 1 MiB"); var decoder=StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); try(var reader=new JsonReader(new StringReader(decoder.decode(ByteBuffer.wrap(raw)).toString()))) { reader.setStrictness(com.google.gson.Strictness.STRICT); var result=value(reader,0); if(reader.peek()!=JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing JSON"); return result; } } public static String encode(Object value) { if(value instanceof NumberToken n) return n.raw(); if(value instanceof Map m) { var keys=new ArrayList(); for(var k:m.keySet()) keys.add((String)k); keys.sort((a,b)->Arrays.compareUnsigned(a.getBytes(StandardCharsets.UTF_8),b.getBytes(StandardCharsets.UTF_8))); var parts=new ArrayList(); for(var k:keys) parts.add(quote(k)+":"+encode(m.get(k))); return "{"+String.join(",",parts)+"}"; } if(value instanceof List l) { var parts=new ArrayList(); for(var child:l) parts.add(encode(child)); return "["+String.join(",",parts)+"]"; } if(value instanceof String s) return quote(s); if(value==null) return "null"; if(value instanceof Boolean b) return b?"true":"false"; throw new IllegalArgumentException("Unsupported value"); } public static byte[] decode(Object input,int size) { if(!(input instanceof String s) || !s.matches("[A-Za-z0-9_-]+")) throw new IllegalArgumentException("Invalid base64url"); var raw=Base64.getUrlDecoder().decode(s); if(raw.length!=size || !Base64.getUrlEncoder().withoutPadding().encodeToString(raw).equals(s)) throw new IllegalArgumentException("Invalid base64url length"); return raw; } } ``` ```java // KDPika signature authenticity only. Java 17+, Gson 2.14.0, BC 1.86. // Run: java -cp classes:gson.jar:bcprov.jar Verify key.json envelope.json // Output signature_valid=true/false. Trust the key separately; enforce policy next. import java.nio.file.*; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.*; import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters; import org.bouncycastle.crypto.signers.Ed25519Signer; public final class Verify { public static boolean verifyEnvelope(byte[] trustedKeyRaw,byte[] envelopeRaw) { try { var key=(Map)Canonical.parse(trustedKeyRaw); var env=(Map)Canonical.parse(envelopeRaw); var publicKey=Canonical.decode(key.get("public_key"),32); var id="ed25519_"+HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(publicKey)).substring(0,16); if(!"Ed25519".equals(key.get("algorithm")) || !"base64url".equals(key.get("encoding")) || !id.equals(key.get("key_id")) || !"Ed25519".equals(env.get("algorithm")) || !id.equals(env.get("key_id")) || !(env.get("payload") instanceof Map)) return false; var message=Canonical.encode(env.get("payload")).getBytes(StandardCharsets.UTF_8); var verifier=new Ed25519Signer(); verifier.init(false,new Ed25519PublicKeyParameters(publicKey,0)); verifier.update(message,0,message.length); return verifier.verifySignature(Canonical.decode(env.get("signature"),64)); } catch(Exception failure) { return false; } } public static void main(String[] args) { boolean valid=false; try { if(args.length==2) valid=verifyEnvelope(Files.readAllBytes(Path.of(args[0])),Files.readAllBytes(Path.of(args[1]))); } catch(Exception failure) { valid=false; } System.out.println("signature_valid="+valid); if(!valid) System.exit(1); } } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Kotlin signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/kotlin --- title: "Kotlin signature verification" description: "Run the complete Kotlin Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/kotlin" section: "Signature examples" order: 207 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Kotlin without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Kotlin verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Kotlin compiler 2.4.21; JDK 17+; Gson 2.14.0; Bouncy Castle 1.86. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p dependencies build/java curl -fSL https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar -o dependencies/gson.jar curl -fSL https://repo.maven.apache.org/maven2/org/bouncycastle/bcprov-jdk18on/1.86/bcprov-jdk18on-1.86.jar -o dependencies/bcprov.jar javac -cp 'dependencies/gson.jar:dependencies/bcprov.jar' -d build/java java/Canonical.java kotlinc -jvm-target 17 -cp 'build/java:dependencies/gson.jar:dependencies/bcprov.jar' -include-runtime -d build/kotlin.jar kotlin/Verify.kt ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The commands use the macOS/Linux classpath separator, a colon. On Windows replace classpath separators with semicolons and quote the complete classpath. Source code is unchanged. JDK21 was used in the recorded test; the source targets Java17. ## Run the positive and negative cases ```bash java -cp 'build/kotlin.jar:build/java:dependencies/gson.jar:dependencies/bcprov.jar' VerifyKt vectors/positive.key.json vectors/positive.envelope.json java -cp 'build/kotlin.jar:build/java:dependencies/gson.jar:dependencies/bcprov.jar' VerifyKt vectors/tampered.key.json vectors/tampered.envelope.json java -cp 'build/kotlin.jar:build/java:dependencies/gson.jar:dependencies/bcprov.jar' VerifyKt vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command java -cp 'build/kotlin.jar:build/java:dependencies/gson.jar:dependencies/bcprov.jar' VerifyKt ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```java // Lossless KDPika JSON tree. Java 17+, Gson 2.14.0. Shared with Kotlin verifier. // JsonReader retains numeric tokens; duplicate properties and bad Unicode fail. import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonToken; import java.io.StringReader; import java.nio.ByteBuffer; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.*; public final class Canonical { private record NumberToken(String raw) {} private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); public static String quote(String value) { for (int i=0;i=value.length() || !Character.isLowSurrogate(value.charAt(i))) throw new IllegalArgumentException("Invalid Unicode"); } else if (Character.isLowSurrogate(c)) throw new IllegalArgumentException("Invalid Unicode"); } return GSON.toJson(value); } private static Object value(JsonReader reader,int depth) throws Exception { if (depth>64) throw new IllegalArgumentException("JSON exceeds 64 levels"); return switch(reader.peek()) { case BEGIN_OBJECT -> { reader.beginObject(); var map=new HashMap(); while(reader.hasNext()) { var key=reader.nextName(); quote(key); if(map.containsKey(key)) throw new IllegalArgumentException("Duplicate JSON property"); map.put(key,value(reader,depth+1)); } reader.endObject(); yield map; } case BEGIN_ARRAY -> { reader.beginArray(); var list=new ArrayList(); while(reader.hasNext()) list.add(value(reader,depth+1)); reader.endArray(); yield list; } case STRING -> { var text=reader.nextString(); quote(text); yield text; } case NUMBER -> new NumberToken(reader.nextString()); case BOOLEAN -> reader.nextBoolean(); case NULL -> { reader.nextNull(); yield null; } default -> throw new IllegalArgumentException("Invalid JSON value"); }; } public static Object parse(byte[] raw) throws Exception { if(raw.length>1048576) throw new IllegalArgumentException("JSON exceeds 1 MiB"); var decoder=StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT); try(var reader=new JsonReader(new StringReader(decoder.decode(ByteBuffer.wrap(raw)).toString()))) { reader.setStrictness(com.google.gson.Strictness.STRICT); var result=value(reader,0); if(reader.peek()!=JsonToken.END_DOCUMENT) throw new IllegalArgumentException("Trailing JSON"); return result; } } public static String encode(Object value) { if(value instanceof NumberToken n) return n.raw(); if(value instanceof Map m) { var keys=new ArrayList(); for(var k:m.keySet()) keys.add((String)k); keys.sort((a,b)->Arrays.compareUnsigned(a.getBytes(StandardCharsets.UTF_8),b.getBytes(StandardCharsets.UTF_8))); var parts=new ArrayList(); for(var k:keys) parts.add(quote(k)+":"+encode(m.get(k))); return "{"+String.join(",",parts)+"}"; } if(value instanceof List l) { var parts=new ArrayList(); for(var child:l) parts.add(encode(child)); return "["+String.join(",",parts)+"]"; } if(value instanceof String s) return quote(s); if(value==null) return "null"; if(value instanceof Boolean b) return b?"true":"false"; throw new IllegalArgumentException("Unsupported value"); } public static byte[] decode(Object input,int size) { if(!(input instanceof String s) || !s.matches("[A-Za-z0-9_-]+")) throw new IllegalArgumentException("Invalid base64url"); var raw=Base64.getUrlDecoder().decode(s); if(raw.length!=size || !Base64.getUrlEncoder().withoutPadding().encodeToString(raw).equals(s)) throw new IllegalArgumentException("Invalid base64url length"); return raw; } } ``` ```kotlin // KDPika signature authenticity only. Kotlin 2.4.21/JVM 17+, Gson 2.14.0, BC 1.86. // Uses adjacent Java Canonical.java. Run VerifyKt key.json envelope.json. // Output signature_valid=true/false. Trust the key separately; enforce policy next. import java.nio.file.Files import java.nio.file.Path import java.security.MessageDigest import java.util.HexFormat import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.signers.Ed25519Signer fun verifyEnvelope(trustedKeyRaw: ByteArray, envelopeRaw: ByteArray): Boolean = try { val key = Canonical.parse(trustedKeyRaw) as Map<*, *> val env = Canonical.parse(envelopeRaw) as Map<*, *> val publicKey = Canonical.decode(key["public_key"], 32) val id = "ed25519_" + HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(publicKey)).take(16) if (key["algorithm"] != "Ed25519" || key["encoding"] != "base64url" || key["key_id"] != id || env["algorithm"] != "Ed25519" || env["key_id"] != id || env["payload"] !is Map<*, *>) false else { val message = Canonical.encode(env["payload"]).toByteArray(Charsets.UTF_8) val verifier = Ed25519Signer() verifier.init(false, Ed25519PublicKeyParameters(publicKey, 0)) verifier.update(message, 0, message.size) verifier.verifySignature(Canonical.decode(env["signature"], 64)) } } catch (failure: Exception) { false } fun main(args: Array) { val valid = try { args.size == 2 && verifyEnvelope(Files.readAllBytes(Path.of(args[0])), Files.readAllBytes(Path.of(args[1]))) } catch (failure: Exception) { false } println("signature_valid=$valid") if (!valid) kotlin.system.exitProcess(1) } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Swift signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/swift --- title: "Swift signature verification" description: "Run the complete Swift Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/swift" section: "Signature examples" order: 208 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Swift without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Swift verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Swift 6+; macOS 13+; built-in CryptoKit. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p build swiftc swift/verify.swift -o build/swift-verify ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. This exact sample uses Apple's CryptoKit and was compiled on macOS. It is not a Windows/Linux SwiftCrypto build; do not claim cross-platform execution without porting and testing the crypto import. ## Run the positive and negative cases ```bash ./build/swift-verify vectors/positive.key.json vectors/positive.envelope.json ./build/swift-verify vectors/tampered.key.json vectors/tampered.envelope.json ./build/swift-verify vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ./build/swift-verify ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```swift // KDPika signature authenticity only. Swift 6+, macOS 13+, CryptoKit. // Run compiled verifier with key.json envelope.json. Output signature_valid=true/false. // Trust the key separately; verify expiry, installation and product before access. import Foundation import CryptoKit enum Invalid: Error { case json } indirect enum JSON { case object([(String, JSON)]), array([JSON]), string(String), number(String), bool(Bool), null func get(_ key: String) throws -> JSON { guard case .object(let pairs) = self, let pair = pairs.first(where: { Array($0.0.utf8) == Array(key.utf8) }) else { throw Invalid.json } return pair.1 } func text() throws -> String { guard case .string(let s) = self else { throw Invalid.json }; return s } } func quote(_ text: String) -> String { var out = "\"" for scalar in text.unicodeScalars { switch scalar.value { case 34: out += "\\\"" case 92: out += "\\\\" case 8: out += "\\b" case 9: out += "\\t" case 10: out += "\\n" case 12: out += "\\f" case 13: out += "\\r" case 0..<32: out += String(format: "\\u%04x", scalar.value) case 0x2028: out += "\\u2028" case 0x2029: out += "\\u2029" default: out.unicodeScalars.append(scalar) } } return out + "\"" } struct Parser { let raw: [UInt8]; var i = 0 init(_ data: Data) throws { guard data.count <= 1048576, String(data: data, encoding: .utf8) != nil else { throw Invalid.json } raw = Array(data) } mutating func ws() { while i < raw.count && [9,10,13,32].contains(raw[i]) { i += 1 } } mutating func string() throws -> String { let start = i; guard i < raw.count && raw[i] == 34 else { throw Invalid.json }; i += 1 while i < raw.count { let c = raw[i]; i += 1 if c == 92 { guard i < raw.count else { throw Invalid.json }; i += 1 } else if c == 34 { let data = Data(raw[start.. JSON { guard depth <= 64 else { throw Invalid.json }; ws(); guard i < raw.count else { throw Invalid.json } let c = raw[i] if c == 34 { return .string(try string()) } if c == 123 || c == 91 { let object = c == 123; let end: UInt8 = object ? 125 : 93; i += 1; ws() var pairs: [(String, JSON)] = []; var values: [JSON] = [] if i < raw.count && raw[i] == end { i += 1; return object ? .object(pairs) : .array(values) } while true { if object { ws(); let key = try string(); ws() guard i < raw.count && raw[i] == 58 && !pairs.contains(where: { Array($0.0.utf8) == Array(key.utf8) }) else { throw Invalid.json } i += 1; pairs.append((key, try value(depth + 1))) } else { values.append(try value(depth + 1)) } ws(); guard i < raw.count else { throw Invalid.json }; let next = raw[i]; i += 1 if next == end { return object ? .object(pairs) : .array(values) } guard next == 44 else { throw Invalid.json } } } for (token, result) in [("true", JSON.bool(true)), ("false", JSON.bool(false)), ("null", JSON.null)] { let bytes = Array(token.utf8) if raw.dropFirst(i).starts(with: bytes) { i += bytes.count; return result } } let rest = String(decoding: raw[i...], as: UTF8.self) guard let range = rest.range(of: #"^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?"#, options: .regularExpression) else { throw Invalid.json } let number = String(rest[range]); i += number.utf8.count; return .number(number) } mutating func parse() throws -> JSON { let result = try value(0); ws(); guard i == raw.count else { throw Invalid.json }; return result } } func canonical(_ value: JSON) -> String { switch value { case .object(let pairs): return "{" + pairs.sorted { Array($0.0.utf8).lexicographicallyPrecedes(Array($1.0.utf8)) }.map { quote($0.0) + ":" + canonical($0.1) }.joined(separator: ",") + "}" case .array(let items): return "[" + items.map(canonical).joined(separator: ",") + "]" case .string(let text): return quote(text) case .number(let raw): return raw case .bool(let value): return value ? "true" : "false" case .null: return "null" } } func decode(_ value: JSON, _ size: Int) throws -> Data { let text = try value.text() guard text.range(of: #"^[A-Za-z0-9_-]+$"#, options: .regularExpression) != nil else { throw Invalid.json } let padded = text.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/") + String(repeating: "=", count: (4 - text.count % 4) % 4) guard let bytes = Data(base64Encoded: padded), bytes.count == size, bytes.base64EncodedString().replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_").replacingOccurrences(of: "=", with: "") == text else { throw Invalid.json } return bytes } func verifyEnvelope(_ keyRaw: Data, _ envRaw: Data) -> Bool { do { var kp = try Parser(keyRaw); var ep = try Parser(envRaw); let key = try kp.parse(); let env = try ep.parse() let bytes = try decode(key.get("public_key"), 32) let id = "ed25519_" + SHA256.hash(data: bytes).map { String(format: "%02x", $0) }.joined().prefix(16) guard try key.get("algorithm").text() == "Ed25519", try key.get("encoding").text() == "base64url", try key.get("key_id").text() == id, try env.get("algorithm").text() == "Ed25519", try env.get("key_id").text() == id else { return false } let payload = try env.get("payload"); guard case .object = payload else { return false } let publicKey = try Curve25519.Signing.PublicKey(rawRepresentation: bytes) return try publicKey.isValidSignature(decode(env.get("signature"), 64), for: Data(canonical(payload).utf8)) } catch { return false } } var valid = false do { if CommandLine.arguments.count == 3 { valid = verifyEnvelope(try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[1])), try Data(contentsOf: URL(fileURLWithPath: CommandLine.arguments[2]))) } } catch { valid = false } print("signature_valid=\(valid)"); if !valid { exit(1) } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Go signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/go --- title: "Go signature verification" description: "Run the complete Go Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/go" section: "Signature examples" order: 209 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Go without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Go verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Go 1.22+; standard library only. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p build go build -o build/go-verify go/verify.go ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash ./build/go-verify vectors/positive.key.json vectors/positive.envelope.json ./build/go-verify vectors/tampered.key.json vectors/tampered.envelope.json ./build/go-verify vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ./build/go-verify ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```go // KDPika signature authenticity only. Go 1.22+, standard library only. // Run: go run verify.go key.json envelope.json. Output signature_valid=true/false. // Independently trust key.json. Check expiry and binding before granting access. package main import ( "bytes" "crypto/ed25519" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io" "os" "sort" "strconv" "strings" "unicode/utf8" ) // encoding/json replaces lone surrogate escapes with U+FFFD. Reject them first // so malformed input cannot verify as a signed literal replacement character. func validEscapes(raw []byte) bool { inString:=false for i:=0;i=len(raw) { return false } if raw[i]!='u' { continue } if i+4>=len(raw) { return false } cp,err:=strconv.ParseUint(string(raw[i+1:i+5]),16,16);if err!=nil { return false };i+=4 if cp>=0xd800 && cp<=0xdbff { if i+6>=len(raw) || raw[i+1]!='\\' || raw[i+2]!='u' { return false } low,err:=strconv.ParseUint(string(raw[i+3:i+7]),16,16) if err!=nil || low<0xdc00 || low>0xdfff { return false };i+=6 } else if cp>=0xdc00 && cp<=0xdfff { return false } } return true } func parse(raw []byte) (any, error) { if len(raw) > 1048576 || !utf8.Valid(raw) || !validEscapes(raw) { return nil, fmt.Errorf("invalid JSON size or Unicode") } decoder := json.NewDecoder(bytes.NewReader(raw)); decoder.UseNumber() var value func(int) (any, error) value = func(depth int) (any, error) { if depth > 64 { return nil, fmt.Errorf("JSON exceeds 64 levels") } token, err := decoder.Token(); if err != nil { return nil, err } if delim, ok := token.(json.Delim); ok { switch delim { case '{': result := map[string]any{} for decoder.More() { keyToken, err := decoder.Token(); if err != nil { return nil, err } key, ok := keyToken.(string); if !ok { return nil, fmt.Errorf("invalid key") } if _, exists := result[key]; exists { return nil, fmt.Errorf("duplicate property") } child, err := value(depth+1); if err != nil { return nil, err }; result[key] = child } end, err := decoder.Token(); if err != nil || end != json.Delim('}') { return nil, fmt.Errorf("invalid object") } return result, nil case '[': result := []any{} for decoder.More() { child, err := value(depth+1); if err != nil { return nil, err }; result = append(result, child) } end, err := decoder.Token(); if err != nil || end != json.Delim(']') { return nil, fmt.Errorf("invalid array") } return result, nil default: return nil, fmt.Errorf("unexpected delimiter") } } return token, nil } result, err := value(0); if err != nil { return nil, err } if _, err = decoder.Token(); err != io.EOF { return nil, fmt.Errorf("trailing JSON") } return result, nil } func quote(value string) string { var out bytes.Buffer; encoder := json.NewEncoder(&out); encoder.SetEscapeHTML(false) if err := encoder.Encode(value); err != nil { panic(err) }; return strings.TrimSuffix(out.String(), "\n") } func canonical(value any) string { switch v := value.(type) { case map[string]any: keys := make([]string,0,len(v)); for k := range v { keys=append(keys,k) }; sort.Strings(keys) parts := make([]string,0,len(keys)); for _,k := range keys { parts=append(parts,quote(k)+":"+canonical(v[k])) } return "{"+strings.Join(parts,",")+"}" case []any: parts:=make([]string,len(v)); for i,child:=range v { parts[i]=canonical(child) }; return "["+strings.Join(parts,",")+"]" case json.Number: return string(v) case string: return quote(v) case bool: if v { return "true" }; return "false" case nil: return "null" default: panic("unsupported JSON value") } } func decode(value any, size int) ([]byte,error) { text,ok:=value.(string); if !ok { return nil,fmt.Errorf("invalid base64url") } raw,err:=base64.RawURLEncoding.Strict().DecodeString(text) if err!=nil || len(raw)!=size || base64.RawURLEncoding.EncodeToString(raw)!=text { return nil,fmt.Errorf("invalid base64url length") }; return raw,nil } func VerifyEnvelope(keyRaw, envRaw []byte) (valid bool) { defer func(){ if recover()!=nil { valid=false } }() k,err:=parse(keyRaw); if err!=nil { return false }; e,err:=parse(envRaw); if err!=nil { return false } key,ok:=k.(map[string]any); if !ok { return false }; env,ok:=e.(map[string]any); if !ok { return false } pub,err:=decode(key["public_key"],32); if err!=nil { return false }; sum:=sha256.Sum256(pub); id:="ed25519_"+hex.EncodeToString(sum[:])[:16] payload,ok:=env["payload"].(map[string]any) if !ok || key["algorithm"]!="Ed25519" || key["encoding"]!="base64url" || key["key_id"]!=id || env["algorithm"]!="Ed25519" || env["key_id"]!=id { return false } signature,err:=decode(env["signature"],64); if err!=nil { return false } return ed25519.Verify(pub,[]byte(canonical(payload)),signature) } func main() { valid:=false if len(os.Args)==3 { key,err1:=os.ReadFile(os.Args[1]); env,err2:=os.ReadFile(os.Args[2]); if err1==nil && err2==nil { valid=VerifyEnvelope(key,env) } } fmt.Printf("signature_valid=%t\n",valid); if !valid { os.Exit(1) } } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Rust signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/rust --- title: "Rust signature verification" description: "Run the complete Rust Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/rust" section: "Signature examples" order: 210 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Rust without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Rust verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Rust 1.81+; exact dependencies in Cargo.toml and included Cargo.lock. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash cargo build --locked --manifest-path rust/Cargo.toml --target-dir build/rust ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash ./build/rust/debug/kdpika-verifier-sample vectors/positive.key.json vectors/positive.envelope.json ./build/rust/debug/kdpika-verifier-sample vectors/tampered.key.json vectors/tampered.envelope.json ./build/rust/debug/kdpika-verifier-sample vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ./build/rust/debug/kdpika-verifier-sample ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```toml [package] name = "kdpika-verifier-sample" version = "1.0.0" edition = "2021" rust-version = "1.81" [dependencies] base64 = "=0.22.1" # Keep the declared Rust 1.81 toolchain usable when resolving transitive crates. base64ct = "=1.6.0" ed25519-dalek = "=2.2.0" serde = "=1.0.228" serde_json = { version = "=1.0.145", features = ["raw_value"] } sha2 = "=0.10.9" zeroize = "=1.8.1" ``` ```rust // KDPika signature authenticity only. Rust 1.81+, pinned Cargo.toml dependencies. // Run: cargo run -- key.json envelope.json. Output signature_valid=true/false. // Trust key.json separately; apply license policy after successful verification. use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use ed25519_dalek::{Signature, VerifyingKey}; use serde::de::{self, Deserialize, Deserializer, MapAccess, Visitor}; use serde_json::value::RawValue; use sha2::{Digest, Sha256}; use std::{collections::BTreeMap, env, fmt, fs}; struct Object(BTreeMap>); impl<'de> Deserialize<'de> for Object { fn deserialize>(deserializer: D) -> Result { struct ObjectVisitor; impl<'de> Visitor<'de> for ObjectVisitor { type Value = Object; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str("unique JSON object") } fn visit_map>(self, mut map: M) -> Result { let mut out = BTreeMap::new(); while let Some((key, value)) = map.next_entry::>()? { if out.insert(key, value).is_some() { return Err(de::Error::custom("duplicate JSON property")); } } Ok(Object(out)) } } deserializer.deserialize_map(ObjectVisitor) } } fn quote(text: &str) -> Result { Ok(serde_json::to_string(text).map_err(|e| e.to_string())?.replace('\u{2028}', "\\u2028").replace('\u{2029}', "\\u2029")) } fn canonical(raw: &RawValue, depth: usize) -> Result { if depth > 64 { return Err("JSON exceeds 64 levels".into()); } let text = raw.get(); match text.as_bytes().first() { Some(b'{') => { let map: Object = serde_json::from_str(text).map_err(|e| e.to_string())?; let mut parts = Vec::new(); for (key, value) in map.0 { parts.push(format!("{}:{}", quote(&key)?, canonical(&value, depth + 1)?)); } Ok(format!("{{{}}}", parts.join(","))) } Some(b'[') => { let list: Vec> = serde_json::from_str(text).map_err(|e| e.to_string())?; let parts: Result,_> = list.iter().map(|v| canonical(v, depth + 1)).collect(); Ok(format!("[{}]", parts?.join(","))) } Some(b'"') => { let value: String = serde_json::from_str(text).map_err(|e| e.to_string())?; quote(&value) } _ => Ok(text.to_owned()), // Validated RawValue; numeric lexemes are never converted to f64. } } fn parse(raw: &[u8]) -> Result { if raw.len() > 1048576 { return Err("JSON exceeds 1 MiB".into()); } let value: Box = serde_json::from_slice(raw).map_err(|e| e.to_string())?; canonical(&value, 0)?; serde_json::from_str(value.get()).map_err(|e| e.to_string()) } fn text(map: &Object, key: &str) -> Result { serde_json::from_str(map.0.get(key).ok_or("Missing property")?.get()).map_err(|e| e.to_string()) } fn decode(value: &str, size: usize) -> Result, String> { let bytes = URL_SAFE_NO_PAD.decode(value).map_err(|e| e.to_string())?; if bytes.len() != size || URL_SAFE_NO_PAD.encode(&bytes) != value { return Err("Invalid base64url length".into()); } Ok(bytes) } pub fn verify_envelope(trusted_key_raw: &[u8], envelope_raw: &[u8]) -> bool { fn verify(key_raw: &[u8], env_raw: &[u8]) -> Result { let key = parse(key_raw)?; let env = parse(env_raw)?; let public = decode(&text(&key,"public_key")?,32)?; let id = format!("ed25519_{:x}",Sha256::digest(&public)); let id = &id[..24]; if text(&key,"algorithm")? != "Ed25519" || text(&key,"encoding")? != "base64url" || text(&key,"key_id")? != id || text(&env,"algorithm")? != "Ed25519" || text(&env,"key_id")? != id { return Ok(false); } let payload = env.0.get("payload").ok_or("Missing payload")?; if !payload.get().starts_with('{') { return Ok(false); } let public_array: [u8;32] = public.try_into().map_err(|_| "Invalid public key")?; let verifier = VerifyingKey::from_bytes(&public_array).map_err(|e| e.to_string())?; let signature = Signature::from_slice(&decode(&text(&env,"signature")?,64)?).map_err(|e| e.to_string())?; Ok(verifier.verify_strict(canonical(payload,0)?.as_bytes(),&signature).is_ok()) } verify(trusted_key_raw,envelope_raw).unwrap_or(false) } fn main() { let args: Vec<_> = env::args().collect(); let mut valid=false; if args.len()==3 { if let (Ok(key),Ok(env))=(fs::read(&args[1]),fs::read(&args[2])) { valid=verify_envelope(&key,&env); } } println!("signature_valid={valid}"); if !valid { std::process::exit(1); } } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Ruby signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/ruby --- title: "Ruby signature verification" description: "Run the complete Ruby Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/ruby" section: "Signature examples" order: 211 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Ruby without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Ruby verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Ruby 2.6+; libsodium 1.0.20; standard Ruby Fiddle/JSON. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash ruby --version # Install libsodium 1.0.20 with your OS package manager. # Set SODIUM_LIBRARY to its full library path if it is not in the loader path. ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. Install libsodium through the operating system's package tooling; it is a client development dependency, not a requirement on KDPika shared hosting. Windows needs a matching libsodium library and compiler include/link paths. Linux uses its own library/package paths. macOS Homebrew may require PKG_CONFIG_PATH pointing at libsodium's lib/pkgconfig directory. Native builds were executed on macOS only. ## Run the positive and negative cases ```bash ruby ruby/verify.rb vectors/positive.key.json vectors/positive.envelope.json ruby ruby/verify.rb vectors/tampered.key.json vectors/tampered.envelope.json ruby ruby/verify.rb vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ruby ruby/verify.rb ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```ruby # KDPika signature authenticity only. Ruby 2.6+, native libsodium 1.0.20. # Run: ruby verify.rb key.json envelope.json. Output signature_valid=true/false. # Set SODIUM_LIBRARY to the installed library path if needed. Trust the key separately. require 'json' require 'strscan' require 'base64' require 'digest' require 'fiddle/import' module Sodium extend Fiddle::Importer dlload ENV.fetch('SODIUM_LIBRARY', RUBY_PLATFORM.include?('darwin') ? 'libsodium.dylib' : 'libsodium.so') extern 'int sodium_init()' extern 'int crypto_sign_verify_detached(void*, void*, unsigned long long, void*)' end NumberToken = Struct.new(:raw) class RawJson def initialize(raw) raise 'Invalid JSON size or UTF-8' if raw.bytesize > 1_048_576 || !raw.force_encoding('UTF-8').valid_encoding? @scanner = StringScanner.new(raw) end def string token = @scanner.scan(/"(?:[^"\\\x00-\x1f]|\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}))*"/) raise 'Invalid JSON string' unless token JSON.parse(token) end def value(depth) raise 'JSON exceeds 64 levels' if depth > 64 @scanner.skip(/[ \t\r\n]*/) return string if @scanner.peek(1) == '"' if ['{', '['].include?(@scanner.peek(1)) object = @scanner.getch == '{'; ending = object ? '}' : ']'; result = object ? {} : [] @scanner.skip(/[ \t\r\n]*/) return result if @scanner.scan(Regexp.new(Regexp.escape(ending))) loop do if object @scanner.skip(/[ \t\r\n]*/); key = string; @scanner.skip(/[ \t\r\n]*/) raise 'Duplicate or invalid property' if result.key?(key) || !@scanner.scan(/:/) result[key] = value(depth + 1) else result << value(depth + 1) end @scanner.skip(/[ \t\r\n]*/); next_char = @scanner.getch return result if next_char == ending raise 'Invalid collection' unless next_char == ',' end end {'true' => true, 'false' => false, 'null' => nil}.each do |token, result| return result if @scanner.scan(Regexp.new(token)) end number = @scanner.scan(/-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/) raise 'Invalid JSON value' unless number NumberToken.new(number) end def parse result = value(0); @scanner.skip(/[ \t\r\n]*/) raise 'Trailing JSON' unless @scanner.eos? result end end def quote(value) raise 'Invalid Unicode' unless value.valid_encoding? JSON.generate(value).gsub("\u2028", '\\u2028').gsub("\u2029", '\\u2029') end def canonical(value) case value when NumberToken then value.raw when Hash then '{' + value.keys.sort_by(&:b).map { |k| quote(k) + ':' + canonical(value[k]) }.join(',') + '}' when Array then '[' + value.map { |child| canonical(child) }.join(',') + ']' when String then quote(value) when TrueClass then 'true' when FalseClass then 'false' when NilClass then 'null' else raise 'Unsupported value' end end def decode(value, size) raise 'Invalid base64url' unless value.is_a?(String) && /\A[A-Za-z0-9_-]+\z/.match?(value) raw = Base64.urlsafe_decode64(value) raise 'Invalid base64url length' unless raw.bytesize == size && Base64.urlsafe_encode64(raw, padding: false) == value raw end def verify_envelope(trusted_key_raw, envelope_raw) key = RawJson.new(trusted_key_raw).parse; env = RawJson.new(envelope_raw).parse public_key = decode(key.fetch('public_key'), 32) id = 'ed25519_' + Digest::SHA256.hexdigest(public_key)[0, 16] return false unless key['algorithm'] == 'Ed25519' && key['encoding'] == 'base64url' && key['key_id'] == id && env['algorithm'] == 'Ed25519' && env['key_id'] == id && env['payload'].is_a?(Hash) message = canonical(env['payload']).b; signature = decode(env.fetch('signature'), 64) Sodium.sodium_init >= 0 && Sodium.crypto_sign_verify_detached(signature, message, message.bytesize, public_key) == 0 rescue StandardError false end if $PROGRAM_NAME == __FILE__ begin valid = ARGV.length == 2 && verify_envelope(File.binread(ARGV[0]), File.binread(ARGV[1])) rescue StandardError valid = false end puts "signature_valid=#{valid}"; exit(valid ? 0 : 1) end ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Dart signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/dart --- title: "Dart signature verification" description: "Run the complete Dart Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/dart" section: "Signature examples" order: 212 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in Dart without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete Dart verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install Dart 3.7+; cryptography 2.9.0. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash cd dart dart pub get cd .. ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. The program uses no OS-specific identity or machine hardware identifier. File paths and executable suffixes may differ on Windows; use your shell's path quoting. ## Run the positive and negative cases ```bash dart run dart/verify.dart vectors/positive.key.json vectors/positive.envelope.json dart run dart/verify.dart vectors/tampered.key.json vectors/tampered.envelope.json dart run dart/verify.dart vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command dart run dart/verify.dart ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```yaml name: kdpika_verifier_sample environment: sdk: '>=3.7.0 <4.0.0' dependencies: cryptography: 2.9.0 ``` ```dart // KDPika signature authenticity only. Dart 3.7+, cryptography 2.9.0. // Run: dart run verify.dart key.json envelope.json. Output signature_valid=true/false. // Trust key.json separately; enforce license policy after successful verification. import 'dart:convert'; import 'dart:io'; import 'package:cryptography/cryptography.dart'; class NumberToken { final String raw; NumberToken(this.raw); } String quote(String value) { for (final cp in value.runes) { if (cp >= 0xd800 && cp <= 0xdfff) throw FormatException('Invalid Unicode'); } return jsonEncode(value).replaceAll('\u2028', r'\u2028').replaceAll('\u2029', r'\u2029'); } class Parser { final String text; int i = 0; Parser(List raw): text = utf8.decode(raw, allowMalformed: false) { if (raw.length > 1048576) throw FormatException('JSON exceeds 1 MiB'); } void ws() { while (i < text.length && ' \t\r\n'.contains(text[i])) { i++; } } String string() { final m = RegExp(r'^"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"').firstMatch(text.substring(i)); if (m == null) throw FormatException('Invalid string'); i += m[0]!.length; final value = jsonDecode(m[0]!) as String; quote(value); return value; } dynamic value(int depth) { if (depth > 64) throw FormatException('JSON exceeds 64 levels'); ws(); if (i >= text.length) throw FormatException('Missing JSON value'); final ch = text[i]; if (ch == '"') return string(); if (ch == '{' || ch == '[') { final object = ch == '{'; final end = object ? '}' : ']'; i++; ws(); final map = {}; final list = []; if (i < text.length && text[i] == end) { i++; return object ? map : list; } while (true) { if (object) { ws(); final key = string(); ws(); if (i >= text.length || text[i++] != ':' || map.containsKey(key)) throw FormatException('Duplicate or invalid property'); map[key] = value(depth + 1); } else { list.add(value(depth + 1)); } ws(); if (i >= text.length) throw FormatException('Incomplete collection'); final next = text[i++]; if (next == end) return object ? map : list; if (next != ',') throw FormatException('Invalid collection'); } } for (final entry in {'true':true, 'false':false, 'null':null}.entries) { if (text.startsWith(entry.key,i)) { i += entry.key.length; return entry.value; } } final m = RegExp(r'^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?').firstMatch(text.substring(i)); if (m == null) throw FormatException('Invalid JSON value'); i += m[0]!.length; return NumberToken(m[0]!); } dynamic parse() { final result = value(0); ws(); if (i != text.length) throw FormatException('Trailing JSON'); return result; } } int compareUtf8(String a, String b) { final x = utf8.encode(a), y = utf8.encode(b); for (var i = 0; i < x.length && i < y.length; i++) { if (x[i] != y[i]) return x[i].compareTo(y[i]); } return x.length.compareTo(y.length); } String canonical(dynamic value) { if (value is NumberToken) return value.raw; if (value is Map) { final keys = value.keys.toList()..sort(compareUtf8); return '{${keys.map((k) => '${quote(k)}:${canonical(value[k])}').join(',')}}'; } if (value is List) return '[${value.map(canonical).join(',')}]'; return value is String ? quote(value) : jsonEncode(value); } List decode(dynamic value, int size) { if (value is! String || !RegExp(r'^[A-Za-z0-9_-]+$').hasMatch(value)) throw FormatException('Invalid base64url'); final raw = base64Url.decode(base64Url.normalize(value)); if (raw.length != size || base64Url.encode(raw).replaceAll('=', '') != value) throw FormatException('Invalid base64url length'); return raw; } Future verifyEnvelope(List trustedKeyRaw, List envRaw) async { try { final key = Parser(trustedKeyRaw).parse() as Map, env = Parser(envRaw).parse() as Map; final public = decode(key['public_key'],32); final hash = await Sha256().hash(public); final id = 'ed25519_${hash.bytes.map((b) => b.toRadixString(16).padLeft(2,'0')).join().substring(0,16)}'; if (key['algorithm'] != 'Ed25519' || key['encoding'] != 'base64url' || key['key_id'] != id || env['algorithm'] != 'Ed25519' || env['key_id'] != id || env['payload'] is! Map) return false; final signature = Signature(decode(env['signature'],64), publicKey: SimplePublicKey(public,type:KeyPairType.ed25519)); return await Ed25519().verify(utf8.encode(canonical(env['payload'])), signature:signature); } catch (_) { return false; } } Future main(List args) async { var valid = false; try { if (args.length == 2) valid = await verifyEnvelope(await File(args[0]).readAsBytes(), await File(args[1]).readAsBytes()); } catch (_) { valid = false; } print('signature_valid=$valid'); if (!valid) exitCode = 1; } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # C signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/c --- title: "C signature verification" description: "Run the complete C Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/c" section: "Signature examples" order: 213 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in C without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete C verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install C11 compiler; yyjson 0.13.0; libsodium 1.0.20. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p dependencies build curl -fSL https://raw.githubusercontent.com/ibireme/yyjson/0.13.0/src/yyjson.h -o dependencies/yyjson.h curl -fSL https://raw.githubusercontent.com/ibireme/yyjson/0.13.0/src/yyjson.c -o dependencies/yyjson.c cc -std=c11 -O2 -Idependencies $(pkg-config --cflags libsodium) c/verify.c dependencies/yyjson.c $(pkg-config --libs libsodium) -o build/c-verify ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. Install libsodium through the operating system's package tooling; it is a client development dependency, not a requirement on KDPika shared hosting. Windows needs a matching libsodium library and compiler include/link paths. Linux uses its own library/package paths. macOS Homebrew may require PKG_CONFIG_PATH pointing at libsodium's lib/pkgconfig directory. Native builds were executed on macOS only. ## Run the positive and negative cases ```bash ./build/c-verify vectors/positive.key.json vectors/positive.envelope.json ./build/c-verify vectors/tampered.key.json vectors/tampered.envelope.json ./build/c-verify vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ./build/c-verify ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```c /* Lossless KDPika JSON canonicalizer. C11+, yyjson 0.13.0, libsodium 1.0.20. * Shared by C and C++ verifiers. No SDK or server behavior changes. * All failures reject; input <=1 MiB, depth <=64, output <=4 MiB. */ #ifndef KDPIKA_CANONICAL_H #define KDPIKA_CANONICAL_H #include #include #include #include #include #include "yyjson.h" typedef struct { char *data; size_t len,cap; int ok; } Output; typedef struct { yyjson_val *key,*value; } Pair; static void append(Output *out,const char *text,size_t len) { if(!out->ok || len>4194304-out->len) { out->ok=0; return; } if(out->len+len+1>out->cap) { size_t cap=(out->len+len+1)*2; char *next=(char*)realloc(out->data,cap); if(!next) { out->ok=0; return; } out->data=next;out->cap=cap; } memcpy(out->data+out->len,text,len);out->len+=len;out->data[out->len]=0; } static void literal(Output *out,const char *text) { append(out,text,strlen(text)); } static void quote(Output *out,const char *text,size_t len) { literal(out,"\""); for(size_t i=0;ikey),ny=yyjson_get_len(y->key),n=nxkey),yyjson_get_str(y->key),n); return cmp?cmp:(nx>ny)-(nx64 || !value) { out->ok=0;return; } if(yyjson_is_obj(value)) { size_t count=yyjson_obj_size(value),i=0;Pair *pairs=(Pair*)calloc(count?count:1,sizeof(Pair)); if(!pairs) { out->ok=0;return; } yyjson_obj_iter iter=yyjson_obj_iter_with(value);yyjson_val *key; while((key=yyjson_obj_iter_next(&iter))) { pairs[i].key=key;pairs[i++].value=yyjson_obj_iter_get_val(key); } qsort(pairs,count,sizeof(Pair),pair_compare);literal(out,"{"); for(i=0;iok=0;break; } literal(out,","); } quote(out,yyjson_get_str(pairs[i].key),yyjson_get_len(pairs[i].key));literal(out,":");canonical(out,pairs[i].value,depth+1); } literal(out,"}");free(pairs); } else if(yyjson_is_arr(value)) { size_t i,max;yyjson_val *child;literal(out,"["); yyjson_arr_foreach(value,i,max,child) { if(i) literal(out,",");canonical(out,child,depth+1); }literal(out,"]"); } else if(yyjson_is_str(value)) quote(out,yyjson_get_str(value),yyjson_get_len(value)); else if(yyjson_is_raw(value)) append(out,yyjson_get_raw(value),yyjson_get_len(value)); else if(yyjson_is_null(value)) literal(out,"null"); else if(yyjson_is_bool(value)) literal(out,yyjson_get_bool(value)?"true":"false"); else out->ok=0; } static yyjson_doc *parse(const char *raw,size_t size) { if(size>1048576) return NULL; yyjson_doc *doc=yyjson_read(raw,size,YYJSON_READ_NUMBER_AS_RAW); if(!doc) return NULL;Output test={NULL,0,0,1};canonical(&test,yyjson_doc_get_root(doc),0);free(test.data); if(!test.ok) { yyjson_doc_free(doc);return NULL; }return doc; } static int equals(yyjson_val *object,const char *name,const char *expected) { yyjson_val *value=yyjson_obj_get(object,name); return yyjson_is_str(value) && yyjson_get_len(value)==strlen(expected) && memcmp(yyjson_get_str(value),expected,strlen(expected))==0; } static int decode(yyjson_val *value,unsigned char *out,size_t size) { if(!yyjson_is_str(value)) return 0;size_t written=0;const char *end=NULL;char encoded[128]; if(sodium_base642bin(out,size,yyjson_get_str(value),yyjson_get_len(value),NULL,&written,&end,sodium_base64_VARIANT_URLSAFE_NO_PADDING)!=0 || written!=size || end!=yyjson_get_str(value)+yyjson_get_len(value)) return 0; sodium_bin2base64(encoded,sizeof encoded,out,size,sodium_base64_VARIANT_URLSAFE_NO_PADDING); return yyjson_get_len(value)==strlen(encoded) && memcmp(encoded,yyjson_get_str(value),strlen(encoded))==0; } static int verify_envelope(const char *key_raw,size_t key_size,const char *env_raw,size_t env_size) { if(sodium_init()<0) return 0; yyjson_doc *kd=parse(key_raw,key_size),*ed=parse(env_raw,env_size);int valid=0; if(kd && ed) { yyjson_val *key=yyjson_doc_get_root(kd),*env=yyjson_doc_get_root(ed);unsigned char pub[32],sig[64],hash[32];char hex[65],id[25]; if(decode(yyjson_obj_get(key,"public_key"),pub,32)) { crypto_hash_sha256(hash,pub,32);sodium_bin2hex(hex,sizeof hex,hash,32);snprintf(id,sizeof id,"ed25519_%.16s",hex); yyjson_val *payload=yyjson_obj_get(env,"payload"); if(equals(key,"algorithm","Ed25519") && equals(key,"encoding","base64url") && equals(key,"key_id",id) && equals(env,"algorithm","Ed25519") && equals(env,"key_id",id) && yyjson_is_obj(payload) && decode(yyjson_obj_get(env,"signature"),sig,64)) { Output message={NULL,0,0,1};canonical(&message,payload,0); valid=message.ok && crypto_sign_verify_detached(sig,(const unsigned char*)message.data,message.len,pub)==0;free(message.data); } } } yyjson_doc_free(kd);yyjson_doc_free(ed);return valid; } #endif ``` ```c /* KDPika signature authenticity only. C11+, yyjson 0.13.0, libsodium 1.0.20. * Run compiled verifier with key.json envelope.json. signature_valid=true/false. * Trust the key independently; enforce license policy before allowing access. */ #include "canonical.h" static char *read_file(const char *path,size_t *size) { FILE *file=fopen(path,"rb");if(!file) return NULL; char *data=(char*)malloc(1048577);if(!data) { fclose(file);return NULL; } *size=fread(data,1,1048577,file);int error=ferror(file);fclose(file); if(error || *size>1048576) { free(data);return NULL; }return data; } int main(int argc,char **argv) { int valid=0; if(argc==3) { size_t ks=0,es=0;char *key=read_file(argv[1],&ks),*env=read_file(argv[2],&es); if(key && env) valid=verify_envelope(key,ks,env,es);free(key);free(env); } printf("signature_valid=%s\n",valid?"true":"false");return valid?0:1; } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # C++ signature verification Canonical: https://licensing.kdpika.com/docs/v1/security/verify-ed25519/cpp --- title: "C++ signature verification" description: "Run the complete C++ Ed25519 verifier against KDPika's real signed JSON and positive, tampered, malformed and expired test cases." slug: "/docs/v1/security/verify-ed25519/cpp" section: "Signature examples" order: 214 audience: "all" level: "advanced" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/security/signed-responses","/docs/v1/security/test-vectors"] draft: false --- Verify KDPika payload authenticity in C++ without losing the server's signed numeric tokens. ## What you will learn - Build and run the complete C++ verifier. - Reject altered, ambiguous and untrusted envelopes. - Keep signature checking separate from permission to use a [license](/docs/v1/start/glossary#license). ## Prerequisites - Read [signed responses](/docs/v1/security/signed-responses). - Install C++17 compiler; yyjson 0.13.0; libsodium 1.0.20. - Download the [complete verification source archive](/docs/downloads/security-verifiers.zip) and extract it. - Open a terminal in the extracted security directory; all paths below are relative to it. - Use Python 3.9+ only for the optional shared test runner, not as a dependency of this verifier. ## Install and build ```bash mkdir -p dependencies build curl -fSL https://raw.githubusercontent.com/ibireme/yyjson/0.13.0/src/yyjson.h -o dependencies/yyjson.h curl -fSL https://raw.githubusercontent.com/ibireme/yyjson/0.13.0/src/yyjson.c -o dependencies/yyjson.c cc -std=c11 -O2 -Idependencies -c dependencies/yyjson.c -o build/yyjson.o c++ -std=c++17 -Idependencies $(pkg-config --cflags libsodium) cpp/verify.cpp build/yyjson.o $(pkg-config --libs libsodium) -o build/cpp-verify ``` Expected result: dependencies resolve and the build exits successfully. Version commands print the installed runtime. Use a maintained runtime compatible with the minimum; these are tested compatibility floors, not claims of ongoing vendor support. Install libsodium through the operating system's package tooling; it is a client development dependency, not a requirement on KDPika shared hosting. Windows needs a matching libsodium library and compiler include/link paths. Linux uses its own library/package paths. macOS Homebrew may require PKG_CONFIG_PATH pointing at libsodium's lib/pkgconfig directory. Native builds were executed on macOS only. ## Run the positive and negative cases ```bash ./build/cpp-verify vectors/positive.key.json vectors/positive.envelope.json ./build/cpp-verify vectors/tampered.key.json vectors/tampered.envelope.json ./build/cpp-verify vectors/expired.key.json vectors/expired.envelope.json python3 selftest.py --command ./build/cpp-verify ``` Expected output for the first three commands is signature_valid=true, signature_valid=false and signature_valid=true. The tampered command exits 1 intentionally; run commands separately rather than stopping a batch at that expected refusal. The shared runner prints PASS23 and exits 0 when every expected acceptance/refusal matches. Expired remains cryptographically authentic but must not unlock access after its expiry. This program returns signature validity only. It does not replace a complete license client, network transport, offline policy or the [official SDK](/docs/v1/start/glossary#official-sdk). ## Complete source ```c /* Lossless KDPika JSON canonicalizer. C11+, yyjson 0.13.0, libsodium 1.0.20. * Shared by C and C++ verifiers. No SDK or server behavior changes. * All failures reject; input <=1 MiB, depth <=64, output <=4 MiB. */ #ifndef KDPIKA_CANONICAL_H #define KDPIKA_CANONICAL_H #include #include #include #include #include #include "yyjson.h" typedef struct { char *data; size_t len,cap; int ok; } Output; typedef struct { yyjson_val *key,*value; } Pair; static void append(Output *out,const char *text,size_t len) { if(!out->ok || len>4194304-out->len) { out->ok=0; return; } if(out->len+len+1>out->cap) { size_t cap=(out->len+len+1)*2; char *next=(char*)realloc(out->data,cap); if(!next) { out->ok=0; return; } out->data=next;out->cap=cap; } memcpy(out->data+out->len,text,len);out->len+=len;out->data[out->len]=0; } static void literal(Output *out,const char *text) { append(out,text,strlen(text)); } static void quote(Output *out,const char *text,size_t len) { literal(out,"\""); for(size_t i=0;ikey),ny=yyjson_get_len(y->key),n=nxkey),yyjson_get_str(y->key),n); return cmp?cmp:(nx>ny)-(nx64 || !value) { out->ok=0;return; } if(yyjson_is_obj(value)) { size_t count=yyjson_obj_size(value),i=0;Pair *pairs=(Pair*)calloc(count?count:1,sizeof(Pair)); if(!pairs) { out->ok=0;return; } yyjson_obj_iter iter=yyjson_obj_iter_with(value);yyjson_val *key; while((key=yyjson_obj_iter_next(&iter))) { pairs[i].key=key;pairs[i++].value=yyjson_obj_iter_get_val(key); } qsort(pairs,count,sizeof(Pair),pair_compare);literal(out,"{"); for(i=0;iok=0;break; } literal(out,","); } quote(out,yyjson_get_str(pairs[i].key),yyjson_get_len(pairs[i].key));literal(out,":");canonical(out,pairs[i].value,depth+1); } literal(out,"}");free(pairs); } else if(yyjson_is_arr(value)) { size_t i,max;yyjson_val *child;literal(out,"["); yyjson_arr_foreach(value,i,max,child) { if(i) literal(out,",");canonical(out,child,depth+1); }literal(out,"]"); } else if(yyjson_is_str(value)) quote(out,yyjson_get_str(value),yyjson_get_len(value)); else if(yyjson_is_raw(value)) append(out,yyjson_get_raw(value),yyjson_get_len(value)); else if(yyjson_is_null(value)) literal(out,"null"); else if(yyjson_is_bool(value)) literal(out,yyjson_get_bool(value)?"true":"false"); else out->ok=0; } static yyjson_doc *parse(const char *raw,size_t size) { if(size>1048576) return NULL; yyjson_doc *doc=yyjson_read(raw,size,YYJSON_READ_NUMBER_AS_RAW); if(!doc) return NULL;Output test={NULL,0,0,1};canonical(&test,yyjson_doc_get_root(doc),0);free(test.data); if(!test.ok) { yyjson_doc_free(doc);return NULL; }return doc; } static int equals(yyjson_val *object,const char *name,const char *expected) { yyjson_val *value=yyjson_obj_get(object,name); return yyjson_is_str(value) && yyjson_get_len(value)==strlen(expected) && memcmp(yyjson_get_str(value),expected,strlen(expected))==0; } static int decode(yyjson_val *value,unsigned char *out,size_t size) { if(!yyjson_is_str(value)) return 0;size_t written=0;const char *end=NULL;char encoded[128]; if(sodium_base642bin(out,size,yyjson_get_str(value),yyjson_get_len(value),NULL,&written,&end,sodium_base64_VARIANT_URLSAFE_NO_PADDING)!=0 || written!=size || end!=yyjson_get_str(value)+yyjson_get_len(value)) return 0; sodium_bin2base64(encoded,sizeof encoded,out,size,sodium_base64_VARIANT_URLSAFE_NO_PADDING); return yyjson_get_len(value)==strlen(encoded) && memcmp(encoded,yyjson_get_str(value),strlen(encoded))==0; } static int verify_envelope(const char *key_raw,size_t key_size,const char *env_raw,size_t env_size) { if(sodium_init()<0) return 0; yyjson_doc *kd=parse(key_raw,key_size),*ed=parse(env_raw,env_size);int valid=0; if(kd && ed) { yyjson_val *key=yyjson_doc_get_root(kd),*env=yyjson_doc_get_root(ed);unsigned char pub[32],sig[64],hash[32];char hex[65],id[25]; if(decode(yyjson_obj_get(key,"public_key"),pub,32)) { crypto_hash_sha256(hash,pub,32);sodium_bin2hex(hex,sizeof hex,hash,32);snprintf(id,sizeof id,"ed25519_%.16s",hex); yyjson_val *payload=yyjson_obj_get(env,"payload"); if(equals(key,"algorithm","Ed25519") && equals(key,"encoding","base64url") && equals(key,"key_id",id) && equals(env,"algorithm","Ed25519") && equals(env,"key_id",id) && yyjson_is_obj(payload) && decode(yyjson_obj_get(env,"signature"),sig,64)) { Output message={NULL,0,0,1};canonical(&message,payload,0); valid=message.ok && crypto_sign_verify_detached(sig,(const unsigned char*)message.data,message.len,pub)==0;free(message.data); } } } yyjson_doc_free(kd);yyjson_doc_free(ed);return valid; } #endif ``` ```cpp // KDPika signature authenticity only. C++17+, yyjson 0.13.0, libsodium 1.0.20. // Uses complete adjacent C canonical.h. Run verifier key.json envelope.json. // Output signature_valid=true/false. Trust the key separately; enforce policy next. #include #include #include #include #include "../c/canonical.h" static std::string read_file(const char *path) { std::ifstream file(path,std::ios::binary);if(!file) throw std::runtime_error("Cannot read file"); std::string data;char buffer[8192]; while(file.read(buffer,sizeof buffer) || file.gcount()) { data.append(buffer,static_cast(file.gcount()));if(data.size()>1048576) throw std::runtime_error("JSON exceeds 1 MiB"); } if(file.bad()) throw std::runtime_error("Cannot read file");return data; } int main(int argc,char **argv) { bool valid=false; try { if(argc==3) { const auto key=read_file(argv[1]),env=read_file(argv[2]); valid=verify_envelope(key.data(),key.size(),env.data(),env.size()); } } catch(const std::exception&) { valid=false; } std::cout<<"signature_valid="<<(valid?"true":"false")<<'\n';return valid?0:1; } ``` ## Expected result All 23 cases pass in the shared runner, including integral floats, non-BMP key order, Unicode separators, duplicate JSON names, invalid signatures and excessive depth. The original response's numeric tokens are retained, not recalculated from binary floating-point values. ## Common mistakes - Taking the verification key from the same untrusted message being checked. - Treating a successful signature as proof of active access, correct installation or recent payment. - Parsing and reserializing numbers before passing the envelope into this sample. ## Security notes The key input must come from your trusted pin or deliberate fixed-origin HTTPS trust policy. Public vector keys are test anchors, never production configuration. This sample fails closed on malformed data; never fall back to unverified [features](/docs/v1/start/glossary#feature). After signature success, check [product](/docs/v1/start/glossary#product), environment, installation hash, active state, start/expiry, application-version policy and signed validation/grace deadlines. See [offline mode](/docs/v1/security/offline-mode). The current official SDKs have documented canonicalization limitations; this independent verifier does not silently repair their caches or alter their public APIs. ## Troubleshooting | Symptom | Cause | Action | | --- | --- | --- | | Positive case fails | Wrong trusted key, changed numeric tokens or missing dependency | Restore the exact archive/vector files and rerun the build | | Tampered case returns false | Expected rejection | Keep the refusal; do not bypass verification | | Expired case returns true | Authenticity and expiry are separate | Reject access in the license policy layer | | Cannot open a file | Terminal is outside the extracted security directory | Change to that directory and retry using quoted paths | ## Next steps Run [all test vectors](/docs/v1/security/test-vectors) and apply the [signed-response policy checks](/docs/v1/security/signed-responses). ## Related - [Verification language index](/docs/v1/security/verify-ed25519) - [Secure token storage](/docs/v1/security/secure-token-storage) # Node.js webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/node --- title: "Node.js webhook signature verifier" description: "Run the complete Node.js raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/node" section: "Webhook verification examples" order: 215 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Node.js verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - Node.js 22 or newer; no packages. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run ```bash node verify.mjs private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```javascript // Notification authenticity, not license validity. Node 22+, no dependencies. // Run: KDP_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET KDP_WEBHOOK_ENVIRONMENT=SANDBOX node verify.mjs body.json headers.json // Expected: verified=true JSON; invalid input prints verified=false and exits 1. import {createHmac, timingSafeEqual} from 'node:crypto'; import {openSync, readSync, closeSync, readFileSync} from 'node:fs'; import {fileURLToPath} from 'node:url'; const contract=JSON.parse(readFileSync(new URL('./contract.json',import.meta.url),'utf8')); const decode=bytes=>new TextDecoder('utf-8',{fatal:true}).decode(bytes); function bounded(path,limit){const fd=openSync(path,'r');try{const out=Buffer.alloc(limit+1);let length=0,read;while(length0)length+=read;if(length>limit)throw Error('Oversized input');return out.subarray(0,length);}finally{closeSync(fd);}} export function verify(body,headers,secret,environment,now){ if(!secret||!contract.environments.includes(environment)||body.length>contract.body_limit)throw Error('Invalid configuration or body size'); const signature=headers?.['x-lmp-signature']; if(typeof signature!=='string')throw Error('Missing signature'); const match=/^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$/.exec(signature); if(!match||match[0]!==signature||Math.abs(now-Number(match[1]))>contract.tolerance_seconds)throw Error('Invalid or stale signature'); const expected=createHmac('sha256',secret).update(match[1]+'.','ascii').update(body).digest(); if(!timingSafeEqual(expected,Buffer.from(match[2],'hex')))throw Error('Signature mismatch'); const event=JSON.parse(decode(body)); if(!event||typeof event!=='object'||Array.isArray(event)||typeof event.id!=='string'||!/^evt_[A-Za-z0-9_-]{1,100}$(?![\s\S])/.test(event.id))throw Error('Invalid event'); if(!contract.events.includes(event.event)||event.environment!==environment||event.id!==headers['x-lmp-event-id']||event.event!==headers['x-lmp-event'])throw Error('Event metadata mismatch'); if(!event.data||typeof event.data!=='object'||Array.isArray(event.data)||typeof event.created_at!=='string'||event.created_at.length===0)throw Error('Invalid envelope'); return event; } if(process.argv[1]===fileURLToPath(import.meta.url)){ try{ if(process.argv.length!==4)throw Error('Two file paths required'); const body=bounded(process.argv[2],contract.body_limit),headers=JSON.parse(decode(bounded(process.argv[3],32768))); const event=verify(body,headers,process.env.KDP_WEBHOOK_SECRET||'',(process.env.KDP_WEBHOOK_ENVIRONMENT||'').toLowerCase(),Math.floor(Date.now()/1000)); console.log(JSON.stringify({verified:true,event_id:event.id,event:event.event,environment:event.environment})); }catch{console.log('{"verified":false}');process.exitCode=1;} } ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # Python webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/python --- title: "Python webhook signature verifier" description: "Run the complete Python raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/python" section: "Webhook verification examples" order: 216 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Python verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - Python 3.11 or newer; standard library only. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run ```bash python3 verify.py private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```python #!/usr/bin/env python3 """Verify a notification, not license validity. Python 3.11+, standard library. Run: KDP_WEBHOOK_SECRET=YOUR_WEBHOOK_SECRET KDP_WEBHOOK_ENVIRONMENT=SANDBOX python3 verify.py body.json headers.json Expected: JSON verified=true for valid bytes; verified=false and exit 1 otherwise. """ import hashlib import hmac import json import os from pathlib import Path import re import sys import time def verify(body: bytes, headers: dict, secret: str, environment: str, now: int) -> dict: contract = json.loads(Path(__file__).with_name('contract.json').read_text()) if not secret or environment not in contract['environments'] or len(body) > contract['body_limit']: raise ValueError('Invalid configuration or body size') signature = headers.get('x-lmp-signature') if not isinstance(signature, str): raise ValueError('Missing signature') match = re.fullmatch(r't=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})', signature) if not match or abs(now - int(match[1])) > contract['tolerance_seconds']: raise ValueError('Invalid or stale signature') message = match[1].encode('ascii') + b'.' + body digest = hmac.new(secret.encode('utf-8'), message, hashlib.sha256).hexdigest() if not hmac.compare_digest(digest, match[2]): raise ValueError('Signature mismatch') event = json.loads(body.decode('utf-8', errors='strict')) if not isinstance(event, dict) or not isinstance(event.get('id'), str): raise ValueError('Invalid event') if not re.fullmatch(r'evt_[A-Za-z0-9_-]{1,100}', event['id']): raise ValueError('Invalid event ID') if event.get('event') not in contract['events'] or event.get('environment') != environment: raise ValueError('Wrong event or environment') if event['id'] != headers.get('x-lmp-event-id') or event['event'] != headers.get('x-lmp-event'): raise ValueError('Header and body mismatch') if not isinstance(event.get('data'), dict) or not isinstance(event.get('created_at'), str) or not event['created_at']: raise ValueError('Invalid event envelope') return event def main() -> int: try: if len(sys.argv) != 3: raise ValueError('Two file paths required') with open(sys.argv[1], 'rb') as stream: body = stream.read(1048577) with open(sys.argv[2], 'rb') as stream: raw_headers = stream.read(32769) if len(raw_headers) > 32768: raise ValueError('Headers too large') headers = json.loads(raw_headers.decode('utf-8', errors='strict')) if not isinstance(headers, dict): raise ValueError('Invalid headers') event = verify(body, headers, os.environ.get('KDP_WEBHOOK_SECRET', ''), os.environ.get('KDP_WEBHOOK_ENVIRONMENT', '').lower(), int(time.time())) print(json.dumps({'verified': True, 'event_id': event['id'], 'event': event['event'], 'environment': event['environment']})) return 0 except (OSError, ValueError, TypeError, KeyError, OverflowError): print('{"verified":false}') return 1 if __name__ == '__main__': raise SystemExit(main()) ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # PHP webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/php --- title: "PHP webhook signature verifier" description: "Run the complete PHP raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/php" section: "Webhook verification examples" order: 217 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete PHP verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - PHP 8.2 or newer with JSON and hash extensions; no Composer packages. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run ```bash php verify.php private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```php $contract['body_limit'])throw new RuntimeException('Invalid configuration or body size'); $signature=$headers['x-lmp-signature']??null; if(!is_string($signature)||!preg_match('/^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$/D',$signature,$parts)||abs($now-(int)$parts[1])>$contract['tolerance_seconds'])throw new RuntimeException('Invalid or stale signature'); if(!hash_equals(hash_hmac('sha256',$parts[1].'.'.$body,$secret),$parts[2]))throw new RuntimeException('Signature mismatch'); $event=json_decode($body,false,64,JSON_THROW_ON_ERROR); if(!is_object($event)||!is_string($event->id??null)||!preg_match('/^evt_[A-Za-z0-9_-]{1,100}$/D',$event->id))throw new RuntimeException('Invalid event'); if(!in_array($event->event??null,$contract['events'],true)||($event->environment??null)!==$environment||$event->id!==($headers['x-lmp-event-id']??null)||$event->event!==($headers['x-lmp-event']??null))throw new RuntimeException('Event metadata mismatch'); if(!is_object($event->data??null)||!is_string($event->created_at??null)||$event->created_at==='')throw new RuntimeException('Invalid envelope'); return json_decode($body,true,64,JSON_THROW_ON_ERROR); } function readKdpBounded(string $path,int $limit): string { $stream=fopen($path,'rb');if(!$stream)throw new RuntimeException('Input unavailable'); try{$value=stream_get_contents($stream,$limit+1);if($value===false||strlen($value)>$limit)throw new RuntimeException('Oversized input');return $value;}finally{fclose($stream);} } if(PHP_SAPI==='cli'&&realpath($_SERVER['SCRIPT_FILENAME'])===__FILE__){ try{ if($argc!==3)throw new RuntimeException('Two file paths required'); $headers=json_decode(readKdpBounded($argv[2],32768),true,64,JSON_THROW_ON_ERROR);if(!is_array($headers))throw new RuntimeException('Invalid headers'); $event=verifyKdpWebhook(readKdpBounded($argv[1],1048576),$headers,(string)getenv('KDP_WEBHOOK_SECRET'),strtolower((string)getenv('KDP_WEBHOOK_ENVIRONMENT')),time()); echo json_encode(['verified'=>true,'event_id'=>$event['id'],'event'=>$event['event'],'environment'=>$event['environment']],JSON_THROW_ON_ERROR).PHP_EOL; }catch(Throwable){echo '{"verified":false}'.PHP_EOL;exit(1);} } ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # C# webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/csharp --- title: "C# webhook signature verifier" description: "Run the complete C# raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/csharp" section: "Webhook verification examples" order: 218 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete C# verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - .NET SDK supporting the net7.0 target; no NuGet dependencies. Verify any supported-runtime target upgrade in your deployment. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run ```bash dotnet build csharp/WebhookVerifier.csproj -c Release dotnet csharp/bin/Release/net7.0/WebhookVerifier.dll private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```xml Exenet7.0 enableenable true ``` ```csharp // Notification authenticity, not license validity. .NET 7+, standard library only. // Build: dotnet build -c Release; run from the parent folder containing contract.json. // Set KDP_WEBHOOK_SECRET/KDP_WEBHOOK_ENVIRONMENT; dotnet csharp/bin/Release/net7.0/WebhookVerifier.dll body.json headers.json // Expected: verified=true JSON; invalid input prints verified=false and exits 1. using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; static class WebhookVerifier { static byte[] Bounded(string path,int limit) { using var file=File.OpenRead(path);using var output=new MemoryStream();var buffer=new byte[8192];int read; while((read=file.Read(buffer,0,Math.Min(buffer.Length,limit+1-(int)output.Length)))>0){output.Write(buffer,0,read);if(output.Length>limit)throw new InvalidDataException();} return output.ToArray(); } static bool Includes(JsonElement array,string value)=>array.EnumerateArray().Any(item=>item.GetString()==value); static string Text(JsonElement obj,string key)=>obj.GetProperty(key).GetString()??throw new InvalidDataException(); public static JsonElement Verify(byte[] body,JsonElement headers,string secret,string environment,long now,JsonElement contract) { if(secret.Length==0||!Includes(contract.GetProperty("environments"),environment)||body.Length>contract.GetProperty("body_limit").GetInt32())throw new InvalidDataException(); var match=Regex.Match(Text(headers,"x-lmp-signature"),@"\At=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})\z",RegexOptions.CultureInvariant); if(!match.Success||Math.Abs(now-long.Parse(match.Groups[1].Value))>contract.GetProperty("tolerance_seconds").GetInt32())throw new InvalidDataException(); var prefix=Encoding.ASCII.GetBytes(match.Groups[1].Value+".");var message=new byte[prefix.Length+body.Length];prefix.CopyTo(message,0);body.CopyTo(message,prefix.Length); using var hmac=new HMACSHA256(Encoding.UTF8.GetBytes(secret)); if(!CryptographicOperations.FixedTimeEquals(hmac.ComputeHash(message),Convert.FromHexString(match.Groups[2].Value)))throw new InvalidDataException(); _=new UTF8Encoding(false,true).GetString(body); using var document=JsonDocument.Parse(body);var item=document.RootElement; if(item.ValueKind!=JsonValueKind.Object||!Regex.IsMatch(Text(item,"id"),@"\Aevt_[A-Za-z0-9_-]{1,100}\z")||!Includes(contract.GetProperty("events"),Text(item,"event"))||Text(item,"environment")!=environment||Text(item,"id")!=Text(headers,"x-lmp-event-id")||Text(item,"event")!=Text(headers,"x-lmp-event"))throw new InvalidDataException(); if(Text(item,"created_at").Length==0||item.GetProperty("data").ValueKind!=JsonValueKind.Object)throw new InvalidDataException(); return item.Clone(); } static int Main(string[] args) { try{ if(args.Length!=2)throw new InvalidDataException(); using var contract=JsonDocument.Parse(Bounded("contract.json",32768));using var headers=JsonDocument.Parse(Bounded(args[1],32768)); var item=Verify(Bounded(args[0],1048576),headers.RootElement,Environment.GetEnvironmentVariable("KDP_WEBHOOK_SECRET")??"",(Environment.GetEnvironmentVariable("KDP_WEBHOOK_ENVIRONMENT")??"").ToLowerInvariant(),DateTimeOffset.UtcNow.ToUnixTimeSeconds(),contract.RootElement); Console.WriteLine(JsonSerializer.Serialize(new{verified=true,event_id=Text(item,"id"),@event=Text(item,"event"),environment=Text(item,"environment")}));return 0; }catch(Exception){Console.WriteLine("{\"verified\":false}");return 1;} } } ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # Go webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/go --- title: "Go webhook signature verifier" description: "Run the complete Go raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/go" section: "Webhook verification examples" order: 219 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Go verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - Go 1.24 or newer; standard library only. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run On Windows build with -o verify-go.exe and run verify-go.exe with the same two request file paths. ```bash go build -o verify-go verify.go ./verify-go private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```go // Notification authenticity, not license validity. Go 1.24+, standard library. // Build: go build -o verify-go verify.go; run from this folder with the two KDP_WEBHOOK environment variables. // Run: ./verify-go body.json headers.json; expected verified=true JSON or verified=false / exit 1. package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "io" "os" "regexp" "strconv" "strings" "time" "unicode/utf8" ) type Contract struct { Events []string `json:"events"` Environments []string `json:"environments"` BodyLimit int64 `json:"body_limit"` Tolerance int64 `json:"tolerance_seconds"` } type Event struct { ID string `json:"id"` Name string `json:"event"` Environment string `json:"environment"` CreatedAt string `json:"created_at"` Data map[string]json.RawMessage `json:"data"` } func bounded(path string, limit int64) ([]byte, error) { file, err := os.Open(path); if err != nil { return nil, err }; defer file.Close() value, err := io.ReadAll(io.LimitReader(file, limit+1)) if err != nil || int64(len(value)) > limit { return nil, errors.New("input unavailable or oversized") } return value, nil } func contains(values []string, candidate string) bool { for _, value := range values { if value == candidate { return true } }; return false } func verify(body []byte, headers map[string]string, secret, environment string, now int64, c Contract) (Event, error) { var event Event fail := errors.New("verification failed") if secret == "" || !contains(c.Environments, environment) || int64(len(body)) > c.BodyLimit { return event, fail } parts := regexp.MustCompile(`^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$`).FindStringSubmatch(headers["x-lmp-signature"]) if parts == nil { return event, fail } stamp, err := strconv.ParseInt(parts[1],10,64); if err != nil || stamp < now-c.Tolerance || stamp > now+c.Tolerance { return event, fail } mac := hmac.New(sha256.New, []byte(secret)); mac.Write([]byte(parts[1]+".")); mac.Write(body) supplied, err := hex.DecodeString(parts[2]); if err != nil || !hmac.Equal(mac.Sum(nil),supplied) { return event, fail } if !utf8.Valid(body) || json.Unmarshal(body, &event) != nil { return event, fail } if !regexp.MustCompile(`^evt_[A-Za-z0-9_-]{1,100}$`).MatchString(event.ID) || !contains(c.Events,event.Name) || event.Environment != environment || event.ID != headers["x-lmp-event-id"] || event.Name != headers["x-lmp-event"] || event.Data == nil || event.CreatedAt == "" { return event, fail } return event, nil } func run() error { if len(os.Args) != 3 { return errors.New("two file paths required") } raw, err := bounded("contract.json",32768); if err != nil { return err }; var c Contract if json.Unmarshal(raw,&c) != nil || c.BodyLimit != 1048576 || c.Tolerance != 300 { return errors.New("invalid contract") } body, err := bounded(os.Args[1],c.BodyLimit); if err != nil { return err } rawHeaders, err := bounded(os.Args[2],32768); if err != nil { return err }; var headers map[string]string if json.Unmarshal(rawHeaders,&headers) != nil { return errors.New("invalid headers") } event, err := verify(body,headers,os.Getenv("KDP_WEBHOOK_SECRET"),strings.ToLower(os.Getenv("KDP_WEBHOOK_ENVIRONMENT")),time.Now().Unix(),c) if err != nil { return err } result := map[string]any{"verified":true,"event_id":event.ID,"event":event.Name,"environment":event.Environment} output, err := json.Marshal(result); if err != nil { return err }; fmt.Println(string(output)); return nil } func main() { if run() != nil { fmt.Println(`{"verified":false}`); os.Exit(1) } } ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # Java webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/java --- title: "Java webhook signature verifier" description: "Run the complete Java raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/java" section: "Webhook verification examples" order: 220 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Java verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - Java 21 with Gson 2.14.0 on both the compile and run classpaths. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run Download [Gson 2.14.0 from the official artifact repository](https://repo1.maven.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar). The tested JAR SHA-256 is 2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f. On Windows replace the classpath separator with a semicolon: -cp ".;gson-2.14.0.jar". ```bash javac -cp gson-2.14.0.jar VerifyWebhook.java java -cp .:gson-2.14.0.jar VerifyWebhook private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```java // Notification authenticity, not license validity. Java 21 + Gson 2.14.0. // Compile: javac -cp gson-2.14.0.jar VerifyWebhook.java // From this folder: java -cp .:gson-2.14.0.jar VerifyWebhook body.json headers.json // Set KDP_WEBHOOK_SECRET/KDP_WEBHOOK_ENVIRONMENT; Windows classpath separator is ;. // Expected: verified=true JSON; invalid input prints verified=false and exits 1. import com.google.gson.*; import java.nio.file.*; import java.nio.charset.*; import java.io.*; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.time.Instant; import java.util.HexFormat; import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; public final class VerifyWebhook { private static byte[] bounded(String path, int limit) throws Exception { try(var input=Files.newInputStream(Path.of(path))){var value=input.readNBytes(limit+1);if(value.length>limit)throw new IOException("Oversized input");return value;} } private static String utf8(byte[] raw) throws Exception { return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(raw)).toString(); } private static String text(JsonObject object,String key) { var value=object.get(key);if(value==null||!value.isJsonPrimitive()||!value.getAsJsonPrimitive().isString())throw new IllegalArgumentException("String required");return value.getAsString(); } private static boolean includes(JsonArray values,String candidate){for(var value:values)if(candidate.equals(value.getAsString()))return true;return false;} public static JsonObject verify(byte[] body,JsonObject headers,String secret,String environment,long now,JsonObject contract) throws Exception { if(secret==null||secret.isEmpty()||!includes(contract.getAsJsonArray("environments"),environment)||body.length>contract.get("body_limit").getAsInt())throw new IllegalArgumentException("Invalid configuration or body size"); var match=Pattern.compile("^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$").matcher(text(headers,"x-lmp-signature")); if(!match.matches()||Math.abs(now-Long.parseLong(match.group(1)))>contract.get("tolerance_seconds").getAsInt())throw new IllegalArgumentException("Invalid or stale signature"); var mac=Mac.getInstance("HmacSHA256");mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8),"HmacSHA256"));mac.update((match.group(1)+".").getBytes(StandardCharsets.US_ASCII)); if(!MessageDigest.isEqual(mac.doFinal(body),HexFormat.of().parseHex(match.group(2))))throw new IllegalArgumentException("Signature mismatch"); var event=JsonParser.parseString(utf8(body)).getAsJsonObject(); if(!text(event,"id").matches("evt_[A-Za-z0-9_-]{1,100}")||!includes(contract.getAsJsonArray("events"),text(event,"event"))||!environment.equals(text(event,"environment"))||!text(event,"id").equals(text(headers,"x-lmp-event-id"))||!text(event,"event").equals(text(headers,"x-lmp-event")))throw new IllegalArgumentException("Event metadata mismatch"); if(text(event,"created_at").isEmpty()||!event.has("data")||!event.get("data").isJsonObject())throw new IllegalArgumentException("Invalid envelope"); return event; } public static void main(String[] args){ try{ if(args.length!=2)throw new IllegalArgumentException("Two file paths required"); var contract=JsonParser.parseString(utf8(bounded("contract.json",32768))).getAsJsonObject(); var headers=JsonParser.parseString(utf8(bounded(args[1],32768))).getAsJsonObject(); var environment=System.getenv("KDP_WEBHOOK_ENVIRONMENT");if(environment==null)environment=""; var event=verify(bounded(args[0],1048576),headers,System.getenv("KDP_WEBHOOK_SECRET"),environment.toLowerCase(java.util.Locale.ROOT),Instant.now().getEpochSecond(),contract); var out=new JsonObject();out.addProperty("verified",true);out.add("event_id",event.get("id"));out.add("event",event.get("event"));out.add("environment",event.get("environment"));System.out.println(out); }catch(Exception failure){System.out.println("{\"verified\":false}");System.exit(1);} } } ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # Ruby webhook signature verifier Canonical: https://licensing.kdpika.com/docs/v1/guides/verify-webhook-signatures/ruby --- title: "Ruby webhook signature verifier" description: "Run the complete Ruby raw-byte notification verifier with authenticated event and environment checks." slug: "/docs/v1/guides/verify-webhook-signatures/ruby" section: "Webhook verification examples" order: 221 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/verify-webhook-signatures", "/docs/v1/guides/test-webhooks-locally"] draft: false --- Authenticate [KDPika Licensing](/docs/v1/start/glossary#kdpika-licensing) notification bytes with the complete Ruby verifier. ## What you will learn - Install and execute this exact sample. - Verify raw bytes, event metadata and the pinned environment. - Identify the boundary between authentication and durable business processing. ## Prerequisites - JSON and OpenSSL standard libraries. Syntax is compatible with Ruby 2.6; use a maintained Ruby release for deployment. - The [notification source archive](/docs/downloads/webhook-receivers.zip), extracted locally. - The [signature contract](/docs/v1/guides/verify-webhook-signatures) and a private [Sandbox](/docs/v1/start/glossary#sandbox) test secret. ## Prepare a request 1. Work inside the extracted [webhooks](/docs/v1/start/glossary#webhook) directory, which contains contract.json. 2. Set KDP_WEBHOOK_SECRET through your private process environment. 3. Set KDP_WEBHOOK_ENVIRONMENT to sandbox. 4. Use Python 3.11+ to create a fresh private synthetic request. ```bash python3 make-test.py events/test-ping.json private/request-1 ``` Expected output: Created private test request files. Use a new directory for each generated request; existing files are not overwritten. The helper signs a captured synthetic body, not a real payment or [license](/docs/v1/start/glossary#license) authorization. Keep contract.json in place: it supplies the shared event list, 300-second receiver freshness policy and 1 MiB receiver body cap. ## Build and run The recorded macOS run uses the legacy Ruby 2.6.10 available locally. Newer OpenSSL bindings use the native fixed_length_secure_compare primitive; the fallback compares all fixed-size positions without early exit but does not prove managed-VM constant-time timing. Prefer the native primitive on a maintained runtime for deployment. ```bash ruby verify.rb private/request-1/body.json private/request-1/headers.json ``` Expected result: exit 0 with verified=true, event_id=evt_ffa11b785e0ede73d9e69801, event=test.ping and environment=sandbox. Property order is not significant. This source was executed in the recorded macOS contract suite; native Windows and Linux execution are not claimed. ## Complete source ```ruby # Notification authenticity, not license validity. Ruby 2.6+, standard library. # Run with KDP_WEBHOOK_SECRET/KDP_WEBHOOK_ENVIRONMENT: ruby verify.rb body.json headers.json # Expected: verified=true JSON; invalid input prints verified=false and exits 1. require 'json' require 'openssl' def verify_kdp_webhook(body, headers, secret, environment, now) contract = JSON.parse(File.read(File.join(__dir__, 'contract.json'))) raise 'Invalid configuration or size' if secret.empty? || !contract['environments'].include?(environment) || body.bytesize > contract['body_limit'] signature = headers['x-lmp-signature'] raise 'Missing signature' unless signature.is_a?(String) match = /\At=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})\z/.match(signature) raise 'Invalid or stale signature' unless match && (now - match[1].to_i).abs <= contract['tolerance_seconds'] expected = OpenSSL::HMAC.hexdigest('SHA256', secret, match[1] + '.' + body) if OpenSSL.respond_to?(:fixed_length_secure_compare) equal = OpenSSL.fixed_length_secure_compare(expected, match[2]) else # Fixed-size, no early exit fallback; VM timing is not a constant-time guarantee. difference = 0 expected.bytes.zip(match[2].bytes).each { |a, b| difference |= a ^ b } equal = difference.zero? end raise 'Signature mismatch' unless equal text = body.dup.force_encoding(Encoding::UTF_8) raise 'Invalid UTF-8' unless text.valid_encoding? event = JSON.parse(text) raise 'Invalid event' unless event.is_a?(Hash) && event['id'].is_a?(String) && /\Aevt_[A-Za-z0-9_-]{1,100}\z/.match(event['id']) raise 'Event metadata mismatch' unless contract['events'].include?(event['event']) && event['environment'] == environment && event['id'] == headers['x-lmp-event-id'] && event['event'] == headers['x-lmp-event'] raise 'Invalid envelope' unless event['data'].is_a?(Hash) && event['created_at'].is_a?(String) && !event['created_at'].empty? event end def read_kdp_bounded(path, limit) value = File.open(path, 'rb') { |stream| stream.read(limit + 1) } || ''.b raise 'Oversized input' if value.bytesize > limit value end if $PROGRAM_NAME == __FILE__ begin raise 'Two file paths required' unless ARGV.length == 2 headers = JSON.parse(read_kdp_bounded(ARGV[1], 32768)) raise 'Invalid headers' unless headers.is_a?(Hash) event = verify_kdp_webhook(read_kdp_bounded(ARGV[0], 1048576), headers, ENV.fetch('KDP_WEBHOOK_SECRET', ''), ENV.fetch('KDP_WEBHOOK_ENVIRONMENT', '').downcase, Time.now.to_i) puts JSON.generate(verified: true, event_id: event['id'], event: event['event'], environment: event['environment']) rescue StandardError puts '{"verified":false}' exit 1 end end ``` ## Connect to an HTTP adapter 1. Read a bounded raw HTTP body before JSON middleware changes it. 2. Reject duplicate signature, event-ID and event-name header fields. 3. Normalize header names to lowercase without changing their values. 4. Pass the exact raw body, headers, configured endpoint secret and expected environment to the verifier. 5. Atomically persist the verified event ID and work item before sending a 2xx response. Expected result: only authenticated recent notifications reach your inbox. The CLI tool does not start an HTTP listener, save deduplication records, call licensing APIs, send email or unlock an application. Use the [local inbox example](/docs/v1/guides/test-webhooks-locally) to test the durable acknowledgement boundary. ## Expected result All captured event types pass when signed with the right current secret and pinned environment. Changed raw bytes, a wrong secret, stale/future attempt timestamp, malformed envelope or wrong environment print exactly {"verified":false} and exit 1. The shared suite exercises positive events, negative input and size boundaries against the exact file above. ## Common mistakes - Reading contract.json from a different working directory or copying only one source without its companion files. - Re-encoding JSON before HMAC verification. - Reusing an expired test signature instead of generating a current one. - Assuming successful authentication deduplicates an additive renewal. ## Security notes Never print the secret or full signature in logs. Use the endpoint's plaintext HMAC secret, not a [developer API key](/docs/v1/start/glossary#developer-api-key) or an Ed25519 public key. A verified notification is not an authoritative license-validity response. Timestamp filtering does not stop duplicate [delivery](/docs/v1/start/glossary#delivery) inside its window; use a durable authenticated event-ID ledger and an independent business-operation key. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | verified=false for an unchanged fixture | Timestamp expired, wrong secret or wrong environment | Regenerate the private request using the intended Sandbox configuration | | Companion file cannot be found | Working directory or extraction incomplete | Restore the complete source archive and work inside webhooks | | The same ID verifies again | Verifier only authenticates bytes | Commit a unique inbox record before applying an effect | ## Next steps Run the [local HTTP, replay and storage-failure tests](/docs/v1/guides/test-webhooks-locally). ## Related - [All verifier languages](/docs/v1/guides/verify-webhook-signatures) - [Delivery and retries](/docs/v1/guides/webhook-delivery) - [Webhook event catalog](/docs/v1/guides/webhook-events) # Node.js store fulfillment reference Canonical: https://licensing.kdpika.com/docs/v1/guides/any-store/node --- title: "Node.js store fulfillment reference" description: "Run the complete Node.js trusted-command receiver and durable licensing worker with private state, encrypted key export and Sandbox evidence." slug: "/docs/v1/guides/any-store/node" section: "Webhooks and integrations" order: 781 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store","/docs/v1/api/idempotency"] draft: false --- Run the complete Node.js reference after your trusted store adapter has independently verified the purchase and authorized one durable licensing effect. ## What you will learn - Install the exact reference dependencies without changing KDPika's application. - Start a loopback receiver and run its bounded worker separately. - Inspect durable outcomes and export a saved encrypted key for private [delivery](/docs/v1/start/glossary#delivery). ## Prerequisites - Read the complete [store backend protocol and mapping guide](/docs/v1/guides/any-store). - Node.js 22.13+ with built-in node:sqlite; no npm packages. SQLite is experimental in this tested Node 22 runtime. - Use a POSIX macOS/Linux-oriented private filesystem, not a public download or temporary state directory. - Configure the private mapping and secret environment variables from the parent guide. - Use a [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product) and backend API key; do not set [Live](/docs/v1/start/glossary#live) confirmation for this test. ## Install and check 1. Download the [complete reference archive](/docs/downloads/store-fulfillment.zip). 2. Check its [SHA-256 source manifest](/docs/fulfillment-examples.json). 3. Extract the archive into a private backend project and enter its fulfillment directory. 4. Replace /private example paths with existing owner-only absolute paths for your backend. 5. Run the following installation/check commands. ```bash node --version node --check node/server.mjs ``` Expected result: The version is at least 22.13 and the syntax check exits 0. An experimental SQLite warning is not a successful fulfillment result. Keep credentials in private environment configuration, not in these commands, source files, terminal screenshots or a browser. This source package has no hidden service installation step and does not alter platform billing. ## Start the receiver ```bash node node/server.mjs serve ``` Expected result: the development receiver listens at HTTP 127.0.0.1:8112. It does not expose a public HTTPS service. For Node/Python/C#, KDP_FULFILLMENT_PORT selects another loopback port; PHP takes the port in its -S command. Keep this process running while sending a command from a separate terminal. Follow [send to the private receiver](/docs/v1/guides/any-store#send-to-the-private-receiver) with your verified private command file. The supplied sender requires PHP/cURL; it is not provider payment verification. A first valid receipt is accepted:true and duplicate:false. No [license](/docs/v1/start/glossary#license) is created until work runs. ## Run the worker ```bash node node/server.mjs work ``` Expected result for one new authorized issue: processed is 1 and states contains DONE:1. A completed unchanged replay processes 0 new effects. On uncertain/failed API outcomes, inspect REVIEW or IN_FLIGHT instead of assuming the operation did nothing. No full [license key](/docs/v1/start/glossary#license-key) is printed. The worker uses atomic worker.lockdir creation because Node core has no flock API. A killed worker leaves the directory, refusing the next run. Confirm the process is stopped and reconcile held effects before removing only the reviewed empty lock directory. Do not automate stale-lock deletion. Each run handles at most 25 ready effects and pauses 1.1 seconds after an attempt. The durable state must persist across cron invocations and deployments. Do not mix language implementations on the same state directory or clear it when rotating the scoped API key. ## Inspect and export ```bash node node/server.mjs inspect ``` Expected result: states maps each existing effect state to its count. Inspection does not retry an API operation or print a complete key. ```bash node node/server.mjs export YOUR_ORDER_ID /private/key.json ``` Expected result after verified DONE issuance and with a new private target path: exported:true and contains_secret:true. The file contains license_id, license_key and email. Export refuses overwrite. This confirms a file was written, not that an email was sent. Authenticate the intended recipient and use your private delivery workflow; never place the export in public_html. ## Complete source The code below is included from the exact files executed by the Sandbox test. Common config.example.json, command.example.json, send-command.php and CONTRACT.md are in the same archive. The parent guide defines their required fields and security boundary in full. ```javascript #!/usr/bin/env node // Trusted fulfillment receiver/worker, not a payment gateway or a browser client. // Node.js 22.13+; built-in SQLite, crypto and HTTP. No npm dependencies. // Private KDP_FULFILLMENT_CONFIG/STATE/SECRET/OUTBOX_KEY and KDP_API_KEY required. // Run: node server.mjs serve | work | inspect | export YOUR_ORDER_ID /private/key.json // Expected: safe receipt/work JSON; full keys only in explicit private exports. import * as fs from 'node:fs'; import path from 'node:path'; import http from 'node:http'; import { createHash, createHmac, timingSafeEqual, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto'; import { DatabaseSync } from 'node:sqlite'; const LIMIT = 262144; const id = value => typeof value === 'string' && /^[A-Za-z0-9_.:-]{1,190}$/.test(value); const object = value => value !== null && typeof value === 'object' && !Array.isArray(value); const sha = value => createHash('sha256').update(value).digest('hex'); const identity = (...values) => sha(values.join('\0')); const fail = code => { throw new Refusal(code); }; const canonical = value => JSON.stringify(sort(value)); const now = () => Math.floor(Date.now() / 1000); const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); class Refusal extends Error {} function sort(value) { if (Array.isArray(value)) return value.map(sort); if (!object(value)) return value; return Object.fromEntries(Object.keys(value).sort().map(key => [key, sort(value[key])])); } class Fulfillment { constructor() { process.umask(0o077); const file = process.env.KDP_FULFILLMENT_CONFIG ?? ''; if (!path.isAbsolute(file)) fail('PRIVATE_ABSOLUTE_CONFIG_REQUIRED'); this.config = JSON.parse(fs.readFileSync(file, 'utf8')); this.directory = process.env.KDP_FULFILLMENT_STATE ?? ''; if (!path.isAbsolute(this.directory)) fail('PRIVATE_ABSOLUTE_STATE_REQUIRED'); if (fs.existsSync(this.directory) && fs.lstatSync(this.directory).isSymbolicLink()) fail('INVALID_STATE_DIRECTORY'); fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 }); if (fs.statSync(this.directory).mode & 0o077) fail('PRIVATE_STATE_PERMISSIONS_REQUIRED'); this.secret = process.env.KDP_FULFILLMENT_SECRET ?? ''; const key = process.env.KDP_FULFILLMENT_OUTBOX_KEY ?? ''; this.outboxKey = Buffer.from(key, 'base64'); if (this.secret.length < 32 || this.outboxKey.length !== 32 || this.outboxKey.toString('base64') !== key) fail('PRIVATE_SECRETS_REQUIRED'); this.environment = this.config.environment; if (!['sandbox', 'live'].includes(this.environment)) fail('INVALID_ENVIRONMENT'); if (this.environment === 'live' && process.env.KDP_CONFIRM_LIVE !== '1') fail('LIVE_CONFIRMATION_REQUIRED'); for (const field of ['source', 'seller_id', 'product_id']) if (!id(this.config[field])) fail('INVALID_CONFIGURATION'); if (!object(this.config.offers) || !Object.keys(this.config.offers).length) fail('OFFERS_REQUIRED'); for (const [offerId, offer] of Object.entries(this.config.offers)) { if (!id(offerId) || !id(offer?.plan_id) || !Number.isInteger(offer.renewal_days) || offer.renewal_days < 1 || offer.renewal_days > 3650) fail('INVALID_OFFER_MAPPING'); } this.base = this.config.base_url.replace(/\/+$/, ''); const url = new URL(this.base); const local = this.environment === 'sandbox' && process.env.KDP_ALLOW_LOOPBACK === '1' && url.protocol === 'http:' && url.hostname === '127.0.0.1'; if (!url.hostname || url.username || url.password || url.search || url.hash || url.pathname !== '/' || (url.protocol !== 'https:' && !local)) fail('INVALID_BASE_URL'); this.apiKey = process.env.KDP_API_KEY ?? ''; if (!this.apiKey.startsWith('lmp_' + this.environment + '_')) fail('API_KEY_ENVIRONMENT_MISMATCH'); const database = path.join(this.directory, 'fulfillment.sqlite'); if (fs.existsSync(database) && fs.lstatSync(database).isSymbolicLink()) fail('INVALID_STATE_FILE'); this.db = new DatabaseSync(database); fs.chmodSync(database, 0o600); this.db.exec(`PRAGMA busy_timeout=3000; CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,effect_key TEXT NOT NULL,received_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS orders(order_key TEXT PRIMARY KEY,offer_id TEXT NOT NULL,email TEXT NOT NULL,license_id TEXT,last_sequence INTEGER NOT NULL DEFAULT 0,held INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS effects(effect_key TEXT PRIMARY KEY,order_key TEXT NOT NULL,sequence INTEGER NOT NULL,command_hash TEXT NOT NULL,command_json TEXT NOT NULL,state TEXT NOT NULL,request_id TEXT,error_code TEXT,created_at INTEGER NOT NULL,UNIQUE(order_key,sequence)); CREATE TABLE IF NOT EXISTS outbox(order_key TEXT PRIMARY KEY,license_id TEXT NOT NULL,cipher TEXT NOT NULL,created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL,outbox_key_hash TEXT NOT NULL);`); const configHash = sha(canonical(this.config)), keyHash = sha(this.outboxKey); this.run('INSERT OR IGNORE INTO binding(id,config_hash,outbox_key_hash) VALUES(1,?,?)', configHash, keyHash); const bound = this.one('SELECT * FROM binding WHERE id=1'); if (bound.config_hash !== configHash || bound.outbox_key_hash !== keyHash) fail('CONFIGURATION_CHANGED_RECONCILE_FIRST'); } run(sql, ...values) { return this.db.prepare(sql).run(...values); } one(sql, ...values) { return this.db.prepare(sql).get(...values); } transaction(operation) { this.db.exec('BEGIN IMMEDIATE'); try { const result = operation(); this.db.exec('COMMIT'); return result; } catch (error) { this.db.exec('ROLLBACK'); throw error; } } validate(event) { const fields = ['version', 'source', 'seller_id', 'environment', 'event_id', 'effect_id', 'order_id', 'sequence', 'action', 'offer_id', 'customer', 'expected_status', 'expected_expires_at']; if (!object(event) || canonical(Object.keys(event).sort()) !== canonical(fields.sort()) || event.version !== 1) fail('INVALID_COMMAND'); for (const field of ['source', 'seller_id', 'environment']) if (event[field] !== this.config[field]) fail('WRONG_SELLER_OR_ENVIRONMENT'); for (const field of ['event_id', 'effect_id', 'order_id', 'offer_id']) if (!id(event[field])) fail('INVALID_IDENTIFIER'); if (!Object.hasOwn(this.config.offers, event.offer_id) || !['issue', 'renew', 'suspend', 'restore', 'revoke', 'record_cancellation'].includes(event.action) || !Number.isInteger(event.sequence) || event.sequence < 1 || event.sequence > 2147483647) fail('UNSUPPORTED_COMMAND'); const customer = event.customer; if (!object(customer) || canonical(Object.keys(customer).sort()) !== '["email","name"]' || typeof customer.email !== 'string' || !/^[^\s@]{1,120}@[^\s@]{1,120}$/.test(customer.email) || customer.email.length > 190 || typeof customer.name !== 'string' || [...customer.name].length > 160) fail('INVALID_CUSTOMER'); if (event.expected_expires_at !== null && (typeof event.expected_expires_at !== 'string' || event.expected_expires_at.length > 40)) fail('INVALID_EXPECTED_EXPIRY'); if (event.action === 'issue') { if (event.sequence !== 1 || event.expected_status !== null || event.expected_expires_at !== null) fail('INVALID_ISSUANCE_COMMAND'); } else if (!['active', 'suspended', 'revoked', 'expired', 'pending', 'cancelled'].includes(event.expected_status)) fail('CURRENT_STATE_REQUIRED'); return event; } accept(body, signature) { const match = /^t=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})$/.exec(signature); if (body.length > LIMIT || !match || Math.abs(now() - Number(match[1])) > 300) fail('INVALID_SIGNATURE'); const actual = createHmac('sha256', this.secret).update(match[1] + '.').update(body).digest(); if (!timingSafeEqual(actual, Buffer.from(match[2], 'hex'))) fail('INVALID_SIGNATURE'); const event = this.validate(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body))); const orderKey = identity(event.source, event.seller_id, event.environment, event.order_id); const effectKey = identity(event.source, event.seller_id, event.environment, event.effect_id); const semantic = { ...event }; delete semantic.event_id; const json = canonical(semantic), commandHash = sha(json), rawHash = sha(body); return this.transaction(() => { const previous = this.one('SELECT body_hash FROM inbox WHERE event_id=?', event.event_id); if (previous) { if (previous.body_hash !== rawHash) fail('EVENT_ID_CONFLICT'); return { accepted: true, duplicate: true }; } const effect = this.one('SELECT command_hash FROM effects WHERE effect_key=?', effectKey); if (effect && effect.command_hash !== commandHash) fail('EFFECT_CONFLICT'); const order = this.one('SELECT * FROM orders WHERE order_key=?', orderKey); if (order && (order.offer_id !== event.offer_id || order.email !== event.customer.email)) fail('ORDER_MAPPING_CONFLICT'); this.run('INSERT OR IGNORE INTO orders(order_key,offer_id,email) VALUES(?,?,?)', orderKey, event.offer_id, event.customer.email); if (!effect) { if (this.one('SELECT effect_key FROM effects WHERE order_key=? AND sequence=?', orderKey, event.sequence)) fail('SEQUENCE_CONFLICT'); this.run("INSERT INTO effects(effect_key,order_key,sequence,command_hash,command_json,state,created_at) VALUES(?,?,?,?,?,'WAITING',?)", effectKey, orderKey, event.sequence, commandHash, json, now()); } this.run('INSERT INTO inbox(event_id,body_hash,effect_key,received_at) VALUES(?,?,?,?)', event.event_id, rawHash, effectKey, now()); return { accepted: true, duplicate: Boolean(effect) }; }); } async api(method, route, body = null, idempotency = null) { const headers = { Authorization: 'Bearer ' + this.apiKey, Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'KDPika-Reference-Fulfillment/1' }; if (idempotency) headers['Idempotency-Key'] = idempotency; const response = await fetch(this.base + '/api/v1' + route, { method, headers, body: body === null ? undefined : canonical(body), redirect: 'manual', signal: AbortSignal.timeout(15000) }); const reader = response.body.getReader(); const chunks = []; let size = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; size += value.length; if (size > 1048576) { await reader.cancel(); fail('UNEXPECTED_API_BODY'); } chunks.push(value); } } finally { reader.releaseLock(); } const data = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks))); if (!object(data)) fail('UNEXPECTED_API_BODY'); return [response.status, data]; } async perform(command, order, effectKey) { if (command.action === 'issue') { if (order.license_id) fail('ORDER_ALREADY_HAS_LICENSE'); const [status, result] = await this.api('POST', '/licenses', { product_id: this.config.product_id, plan_id: this.config.offers[command.offer_id].plan_id, customer: command.customer }, 'store_' + effectKey); const item = result.license; if (status !== 201 || result.success !== true || !object(item) || typeof item.id !== 'string' || !/^lic_[A-Za-z0-9_-]{1,100}$/.test(item.id) || typeof item.license_key !== 'string' || item.license_key.length < 12 || item.license_key.length > 100 || item.environment !== this.environment) fail('ISSUANCE_RESULT_REQUIRES_REVIEW'); return [item.id, item.license_key, result.request_id ?? null]; } if (!order.license_id) fail('ISSUE_FIRST'); const route = '/licenses/' + encodeURIComponent(order.license_id); const [status, current] = await this.api('GET', route), item = current.license; if (status !== 200 || current.success !== true || !object(item) || item.id !== order.license_id || item.environment !== this.environment || item.status !== command.expected_status || item.expires_at !== command.expected_expires_at) fail('LICENSE_CHANGED_RECONCILE_FIRST'); if (command.action === 'record_cancellation') return [item.id, null, current.request_id ?? null]; const body = command.action === 'renew' ? { days: this.config.offers[command.offer_id].renewal_days } : { reason: 'Verified store fulfillment decision' }; const [changedStatus, changed] = await this.api('POST', route + '/' + command.action, body); const expected = { suspend: 'suspended', restore: 'active', revoke: 'revoked', renew: item.status === 'expired' ? 'active' : item.status }[command.action]; if (changedStatus !== 200 || changed.success !== true || changed.license?.id !== item.id || changed.license.status !== expected || (command.action === 'renew' && typeof changed.license.expires_at !== 'string')) fail('MUTATION_RESULT_REQUIRES_REVIEW'); return [item.id, null, changed.request_id ?? null]; } async work() { // Node core has no flock. An atomic directory lock fails closed after a crash. // Never remove a stale lock before confirming its worker is no longer running. const lock = path.join(this.directory, 'worker.lockdir'); try { fs.mkdirSync(lock, { mode: 0o700 }); } catch { fail('WORKER_BUSY_OR_STALE_LOCK'); } let processed = 0; try { while (processed < 25) { const claimed = this.transaction(() => { const effect = this.one("SELECT e.* FROM effects e JOIN orders o ON o.order_key=e.order_key WHERE e.state='WAITING' AND o.held=0 AND e.sequence=o.last_sequence+1 ORDER BY e.created_at,e.effect_key LIMIT 1"); if (!effect) return null; const order = this.one('SELECT * FROM orders WHERE order_key=?', effect.order_key); this.run("UPDATE effects SET state='IN_FLIGHT' WHERE effect_key=?", effect.effect_key); this.run('UPDATE orders SET held=1 WHERE order_key=?', effect.order_key); return [effect, order]; }); if (!claimed) break; const [effect, order] = claimed; try { const [licenseId, key, requestId] = await this.perform(JSON.parse(effect.command_json), order, effect.effect_key); if (requestId !== null && (typeof requestId !== 'string' || requestId.length > 190)) fail('UNEXPECTED_REQUEST_ID'); this.transaction(() => { if (key !== null) { const nonce = randomBytes(12), cipher = createCipheriv('aes-256-gcm', this.outboxKey, nonce); cipher.setAAD(Buffer.from(effect.order_key)); const bytes = Buffer.concat([nonce, cipher.update(key, 'utf8'), cipher.final(), cipher.getAuthTag()]); this.run('INSERT INTO outbox(order_key,license_id,cipher,created_at) VALUES(?,?,?,?)', effect.order_key, licenseId, bytes.toString('base64'), now()); } this.run("UPDATE effects SET state='DONE',request_id=? WHERE effect_key=?", requestId, effect.effect_key); this.run('UPDATE orders SET license_id=?,last_sequence=?,held=0 WHERE order_key=?', licenseId, effect.sequence, effect.order_key); }); } catch { this.run("UPDATE effects SET state='REVIEW',error_code='RECONCILE_BEFORE_RETRY' WHERE effect_key=?", effect.effect_key); } processed++; await delay(1100); } } finally { fs.rmdirSync(lock); } return { processed, states: this.inspect() }; } inspect() { return Object.fromEntries(this.db.prepare('SELECT state,COUNT(*) AS count FROM effects GROUP BY state').all().map(row => [row.state, row.count])); } export(orderId, target) { if (!id(orderId)) fail('INVALID_ORDER_ID'); const orderKey = identity(this.config.source, this.config.seller_id, this.environment, orderId); const item = this.one('SELECT b.*,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key WHERE b.order_key=?', orderKey); if (!item) fail('NO_SAVED_KEY'); const parent = path.dirname(target); if (!path.isAbsolute(target) || !fs.statSync(parent).isDirectory() || (fs.statSync(parent).mode & 0o077)) fail('PRIVATE_EXPORT_DIRECTORY_REQUIRED'); const raw = Buffer.from(item.cipher, 'base64'); if (raw.length < 29) fail('INVALID_OUTBOX'); const cipher = createDecipheriv('aes-256-gcm', this.outboxKey, raw.subarray(0, 12)); cipher.setAAD(Buffer.from(orderKey)); cipher.setAuthTag(raw.subarray(-16)); const key = Buffer.concat([cipher.update(raw.subarray(12, -16)), cipher.final()]).toString('utf8'); fs.writeFileSync(target, canonical({ license_id: item.license_id, license_key: key, email: item.email }), { flag: 'wx', mode: 0o600 }); return { exported: true, contains_secret: true }; } serve() { const reply = (response, status, value) => { const body = canonical(value); response.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', Connection: 'close' }); response.end(body); }; const server = http.createServer((request, response) => { if (request.method !== 'POST' || request.url !== '/store-event') return reply(response, 404, { accepted: false }); const counts = name => request.rawHeaders.filter((_, index, values) => index % 2 === 0 && values[index].toLowerCase() === name).length; const length = request.headers['content-length']; if (request.headers['transfer-encoding'] || counts('content-length') !== 1 || !/^\d+$/.test(length ?? '')) return reply(response, 400, { accepted: false }); if (Number(length) > LIMIT) return reply(response, 413, { accepted: false }); if (counts('x-fulfillment-signature') !== 1) return reply(response, 401, { accepted: false }); if ((request.headers['content-type'] ?? '').split(';')[0].trim().toLowerCase() !== 'application/json') return reply(response, 415, { accepted: false }); const chunks = []; let size = 0; request.on('data', chunk => { size += chunk.length; if (size <= LIMIT) chunks.push(chunk); }); request.on('end', () => { if (size > LIMIT) return reply(response, 413, { accepted: false }); if (size !== Number(length)) return reply(response, 400, { accepted: false }); try { reply(response, 202, this.accept(Buffer.concat(chunks), request.headers['x-fulfillment-signature'] ?? '')); } catch (error) { reply(response, error instanceof Refusal ? (error.message === 'INVALID_SIGNATURE' ? 401 : 409) : (error instanceof SyntaxError || error instanceof TypeError ? 400 : 503), { accepted: false }); } }); request.on('error', () => response.destroy()); }); server.requestTimeout = 5000; server.headersTimeout = 5000; server.keepAliveTimeout = 1000; server.listen(Number(process.env.KDP_FULFILLMENT_PORT ?? 8112), '127.0.0.1', () => console.log('{"listening":true,"loopback_only":true}')); } } try { const app = new Fulfillment(), args = process.argv.slice(2); if (args.length === 1 && args[0] === 'serve') app.serve(); else if (args.length === 1 && args[0] === 'work') console.log(canonical(await app.work())); else if (args.length === 1 && args[0] === 'inspect') console.log(canonical({ states: app.inspect() })); else if (args.length === 3 && args[0] === 'export') console.log(canonical(app.export(args[1], args[2]))); else fail('USE_SERVE_WORK_INSPECT_OR_EXPORT'); } catch { console.log('{"completed":false,"error":"REVIEW_PRIVATE_CONFIGURATION_OR_STATE"}'); process.exitCode = 1; } ``` ## Expected result The actual local receiver and worker pass the shared lifecycle suite: durable receipt before work, authentic command acceptance, tamper/time/mode/mapping refusal, duplicate/conflicting effects, ordered gap handling, real creation and recipient [activation](/docs/v1/start/glossary#activation), renewal, suspension, restoration and terminal revocation. The suite also exports an encrypted saved key, refuses unsafe config/key drift, restarts the receiver, holds a simulated interrupted intent, and hides a genuinely successful remote create response behind a failing gateway. No second create is sent after that unknown result. These are exact-source local Sandbox tests on macOS. They are not an external store purchase, production deployment, native Windows test, email delivery test or promise of cross-system exactly-once behavior. The known API replay limitations remain unchanged. The read-before-write expected-state check is not an atomic remote compare-and-swap. ## Common mistakes - Sending a provider's native event directly here: this receiver accepts your verified adapter's private command format, not a Stripe/Paddle/PayPal callback. - Exposing the development server publicly without TLS, request bounds and rate protection. - Treating a 202 receipt or exported key file as proof of successful buyer email delivery. - Reusing a fresh state directory after an unknown create instead of reconciling the original order. ## Security notes - Keep config, state, encrypted-ledger backups and plaintext exports outside all web roots. - A backend that holds both the encryption key and database can decrypt its outbox; filesystem and process isolation still matter. - Keep one reviewed writer per order and preserve stable business-effect identity across delivery retries. - PHP SAPI can buffer bodies before script execution. Configure the ingress 256 KiB body limit and timeouts, not only application validation. - Recheck runtime support and dependency advisories before production deployment. The recorded advisory scan is not certification. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Safe configuration error | Missing/mismatched private mapping, key, permissions or runtime dependency | Verify private environment and required modules; never publish raw secrets to debug | | HTTP 401 | Wrong signature, timestamp, changed bytes or duplicate signature header | Sign exact bytes with the separate command secret and verify the clock | | HTTP 409 | Mapping/merchant/effect/sequence conflict | Reconcile trusted adapter state; do not invent a different effect ID | | No work processed | Already complete, sequence gap, held order or busy worker | Inspect saved state and authorized history before any write | | Export refused | Wrong order/key, unsafe target directory or file already exists | Verify recipient/order and use a new private owner-only target file | ## Next steps Review [the parent security checklist](/docs/v1/guides/any-store#security-notes), then test your real provider-specific adapter separately in its test environment. ## Related - [Generic store mapping and lifecycle](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Manual delivery and customer lookup](/docs/v1/guides/manual-issuance) # Python store fulfillment reference Canonical: https://licensing.kdpika.com/docs/v1/guides/any-store/python --- title: "Python store fulfillment reference" description: "Run the complete Python trusted-command receiver and durable licensing worker with private state, encrypted key export and Sandbox evidence." slug: "/docs/v1/guides/any-store/python" section: "Webhooks and integrations" order: 782 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store","/docs/v1/api/idempotency"] draft: false --- Run the complete Python reference after your trusted store adapter has independently verified the purchase and authorized one durable licensing effect. ## What you will learn - Install the exact reference dependencies without changing KDPika's application. - Start a loopback receiver and run its bounded worker separately. - Inspect durable outcomes and export a saved encrypted key for private [delivery](/docs/v1/start/glossary#delivery). ## Prerequisites - Read the complete [store backend protocol and mapping guide](/docs/v1/guides/any-store). - Python 3.11+, SQLite, POSIX fcntl, and the exact cryptography/cffi/pycparser versions from python/requirements.txt. - Use a POSIX macOS/Linux-oriented private filesystem, not a public download or temporary state directory. - Configure the private mapping and secret environment variables from the parent guide. - Use a [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product) and backend API key; do not set [Live](/docs/v1/start/glossary#live) confirmation for this test. ## Install and check 1. Download the [complete reference archive](/docs/downloads/store-fulfillment.zip). 2. Check its [SHA-256 source manifest](/docs/fulfillment-examples.json). 3. Extract the archive into a private backend project and enter its fulfillment directory. 4. Replace /private example paths with existing owner-only absolute paths for your backend. 5. Run the following installation/check commands. ```bash python3 -m venv /private/kdpika-fulfillment-venv /private/kdpika-fulfillment-venv/bin/python -m pip install -r python/requirements.txt /private/kdpika-fulfillment-venv/bin/python -m pip check ``` Expected result: Dependency installation and pip check exit 0. The tested requirements pin cryptography 50.0.2, cffi 2.1.1 and pycparser 3.11. Use a supported Python version; do not modify the platform SDK environment. Keep credentials in private environment configuration, not in these commands, source files, terminal screenshots or a browser. This source package has no hidden service installation step and does not alter platform billing. ## Start the receiver ```bash /private/kdpika-fulfillment-venv/bin/python python/server.py serve ``` Expected result: the development receiver listens at HTTP 127.0.0.1:8112. It does not expose a public HTTPS service. For Node/Python/C#, KDP_FULFILLMENT_PORT selects another loopback port; PHP takes the port in its -S command. Keep this process running while sending a command from a separate terminal. Follow [send to the private receiver](/docs/v1/guides/any-store#send-to-the-private-receiver) with your verified private command file. The supplied sender requires PHP/cURL; it is not provider payment verification. A first valid receipt is accepted:true and duplicate:false. No [license](/docs/v1/start/glossary#license) is created until work runs. ## Run the worker ```bash /private/kdpika-fulfillment-venv/bin/python python/server.py work ``` Expected result for one new authorized issue: processed is 1 and states contains DONE:1. A completed unchanged replay processes 0 new effects. On uncertain/failed API outcomes, inspect REVIEW or IN_FLIGHT instead of assuming the operation did nothing. No full [license key](/docs/v1/start/glossary#license-key) is printed. The worker uses nonblocking POSIX flock on worker.lock. Another active worker is refused. An interrupted remote call leaves its effect IN_FLIGHT and the order held even when the operating system releases the process lock. Reconcile the remote result before another mutation. Each run handles at most 25 ready effects and pauses 1.1 seconds after an attempt. The durable state must persist across cron invocations and deployments. Do not mix language implementations on the same state directory or clear it when rotating the scoped API key. ## Inspect and export ```bash /private/kdpika-fulfillment-venv/bin/python python/server.py inspect ``` Expected result: states maps each existing effect state to its count. Inspection does not retry an API operation or print a complete key. ```bash /private/kdpika-fulfillment-venv/bin/python python/server.py export YOUR_ORDER_ID /private/key.json ``` Expected result after verified DONE issuance and with a new private target path: exported:true and contains_secret:true. The file contains license_id, license_key and email. Export refuses overwrite. This confirms a file was written, not that an email was sent. Authenticate the intended recipient and use your private delivery workflow; never place the export in public_html. ## Complete source The code below is included from the exact files executed by the Sandbox test. Common config.example.json, command.example.json, send-command.php and CONTRACT.md are in the same archive. The parent guide defines their required fields and security boundary in full. ```python #!/usr/bin/env python3 """Durable trusted-store command receiver and licensing worker; not a payment gateway. Python 3.11+, cryptography==50.0.2. POSIX file locking, private SQLite/AES-GCM outbox. Set KDP_FULFILLMENT_CONFIG/STATE/SECRET/OUTBOX_KEY and KDP_API_KEY privately. Run: python3 server.py serve | work | inspect | export YOUR_ORDER_ID /private/key.json Expected: safe acceptance/work state JSON, never a complete key in stdout. """ import base64 from contextlib import contextmanager import fcntl import hashlib import hmac from http.server import BaseHTTPRequestHandler, HTTPServer import json import os from pathlib import Path import re import sqlite3 import sys import time import urllib.error import urllib.parse import urllib.request from cryptography.hazmat.primitives.ciphers.aead import AESGCM LIMIT = 262144 ID = re.compile(r'[A-Za-z0-9_.:-]{1,190}') ACTIONS = {'issue', 'renew', 'suspend', 'restore', 'revoke', 'record_cancellation'} STATES = {'active', 'suspended', 'revoked', 'expired', 'pending', 'cancelled'} FIELDS = {'version', 'source', 'seller_id', 'environment', 'event_id', 'effect_id', 'order_id', 'sequence', 'action', 'offer_id', 'customer', 'expected_status', 'expected_expires_at'} def encoded(value): return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(',', ':'), allow_nan=False) def identity(*values): return hashlib.sha256('\0'.join(values).encode()).hexdigest() class Refusal(Exception): pass def decoded(raw): def unique(pairs): result = {} for key, value in pairs: if key in result: raise Refusal('DUPLICATE_JSON_MEMBER') result[key] = value return result def invalid_constant(value): raise Refusal('NONSTANDARD_JSON_NUMBER') return json.loads(raw, object_pairs_hook=unique, parse_constant=invalid_constant) class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, request, response, code, message, headers, new_url): return None class Fulfillment: def __init__(self): os.umask(0o077) config_path = Path(os.environ['KDP_FULFILLMENT_CONFIG']) if not config_path.is_absolute(): raise Refusal('PRIVATE_ABSOLUTE_CONFIG_REQUIRED') self.config = decoded(config_path.read_text()) self.directory = Path(os.environ['KDP_FULFILLMENT_STATE']) if not self.directory.is_absolute() or self.directory.is_symlink(): raise Refusal('PRIVATE_ABSOLUTE_STATE_REQUIRED') self.directory.mkdir(parents=True, mode=0o700, exist_ok=True) if self.directory.stat().st_mode & 0o077: raise Refusal('PRIVATE_STATE_PERMISSIONS_REQUIRED') self.secret = os.environ['KDP_FULFILLMENT_SECRET'] self.outbox_key = base64.b64decode(os.environ['KDP_FULFILLMENT_OUTBOX_KEY'], validate=True) if len(self.secret) < 32 or len(self.outbox_key) != 32: raise Refusal('PRIVATE_SECRETS_REQUIRED') self.environment = self.config['environment'] if self.environment not in ('sandbox', 'live'): raise Refusal('INVALID_ENVIRONMENT') if self.environment == 'live' and os.environ.get('KDP_CONFIRM_LIVE') != '1': raise Refusal('LIVE_CONFIRMATION_REQUIRED') for field in ('source', 'seller_id', 'product_id'): if not isinstance(self.config[field], str) or not ID.fullmatch(self.config[field]): raise Refusal('INVALID_CONFIGURATION') if not isinstance(self.config['offers'], dict) or not self.config['offers']: raise Refusal('OFFERS_REQUIRED') for offer_id, offer in self.config['offers'].items(): if not ID.fullmatch(offer_id) or not ID.fullmatch(offer['plan_id']) or type(offer['renewal_days']) is not int or not 1 <= offer['renewal_days'] <= 3650: raise Refusal('INVALID_OFFER_MAPPING') self.base = self.config['base_url'].rstrip('/') url = urllib.parse.urlsplit(self.base) local = self.environment == 'sandbox' and os.environ.get('KDP_ALLOW_LOOPBACK') == '1' and url.scheme == 'http' and url.hostname == '127.0.0.1' if url.username or url.password or url.query or url.fragment or url.path or not url.hostname or (url.scheme != 'https' and not local): raise Refusal('INVALID_BASE_URL') self.api_key = os.environ['KDP_API_KEY'] if not self.api_key.startswith('lmp_' + self.environment + '_'): raise Refusal('API_KEY_ENVIRONMENT_MISMATCH') self.database = self.directory / 'fulfillment.sqlite' if self.database.is_symlink(): raise Refusal('INVALID_STATE_FILE') with self.connect() as db: db.executescript(''' CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,effect_key TEXT NOT NULL,received_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS orders(order_key TEXT PRIMARY KEY,offer_id TEXT NOT NULL,email TEXT NOT NULL,license_id TEXT,last_sequence INTEGER NOT NULL DEFAULT 0,held INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS effects(effect_key TEXT PRIMARY KEY,order_key TEXT NOT NULL,sequence INTEGER NOT NULL,command_hash TEXT NOT NULL,command_json TEXT NOT NULL,state TEXT NOT NULL,request_id TEXT,error_code TEXT,created_at INTEGER NOT NULL,UNIQUE(order_key,sequence)); CREATE TABLE IF NOT EXISTS outbox(order_key TEXT PRIMARY KEY,license_id TEXT NOT NULL,cipher TEXT NOT NULL,created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL,outbox_key_hash TEXT NOT NULL); ''') config_hash = hashlib.sha256(encoded(self.config).encode()).hexdigest() key_hash = hashlib.sha256(self.outbox_key).hexdigest() db.execute('INSERT OR IGNORE INTO binding(id,config_hash,outbox_key_hash) VALUES(1,?,?)', (config_hash, key_hash)) binding = db.execute('SELECT * FROM binding WHERE id=1').fetchone() if binding['config_hash'] != config_hash or binding['outbox_key_hash'] != key_hash: raise Refusal('CONFIGURATION_CHANGED_RECONCILE_FIRST') self.database.chmod(0o600) @contextmanager def connect(self): db = sqlite3.connect(self.database, timeout=3) db.row_factory = sqlite3.Row try: with db: yield db finally: db.close() def validate(self, event): if not isinstance(event, dict) or set(event) != FIELDS or event['version'] != 1 or type(event['version']) is not int: raise Refusal('INVALID_COMMAND') for key in ('source', 'seller_id', 'environment'): if event[key] != self.config[key]: raise Refusal('WRONG_SELLER_OR_ENVIRONMENT') for key in ('event_id', 'effect_id', 'order_id', 'offer_id'): if not isinstance(event[key], str) or not ID.fullmatch(event[key]): raise Refusal('INVALID_IDENTIFIER') if event['offer_id'] not in self.config['offers'] or event['action'] not in ACTIONS or type(event['sequence']) is not int or not 1 <= event['sequence'] <= 2147483647: raise Refusal('UNSUPPORTED_COMMAND') customer = event['customer'] if not isinstance(customer, dict) or set(customer) != {'email', 'name'} or not isinstance(customer['email'], str) or not re.fullmatch(r'[^\s@]{1,120}@[^\s@]{1,120}', customer['email']) or len(customer['email']) > 190 or not isinstance(customer['name'], str) or len(customer['name']) > 160: raise Refusal('INVALID_CUSTOMER') expiry = event['expected_expires_at'] if expiry is not None and (not isinstance(expiry, str) or len(expiry) > 40): raise Refusal('INVALID_EXPECTED_EXPIRY') if event['action'] == 'issue': if event['sequence'] != 1 or event['expected_status'] is not None or expiry is not None: raise Refusal('INVALID_ISSUANCE_COMMAND') elif event['expected_status'] not in STATES: raise Refusal('CURRENT_STATE_REQUIRED') return event def accept(self, body, signature): match = re.fullmatch(r't=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})', signature) if len(body) > LIMIT or not match or abs(int(time.time()) - int(match[1])) > 300: raise Refusal('INVALID_SIGNATURE') digest = hmac.new(self.secret.encode(), match[1].encode() + b'.' + body, hashlib.sha256).hexdigest() if not hmac.compare_digest(digest, match[2]): raise Refusal('INVALID_SIGNATURE') event = self.validate(decoded(body.decode('utf-8', errors='strict'))) order_key = identity(event['source'], event['seller_id'], event['environment'], event['order_id']) effect_key = identity(event['source'], event['seller_id'], event['environment'], event['effect_id']) # Different provider deliveries may carry the same durable effect; event ID is not the effect key. semantic = {key: value for key, value in event.items() if key != 'event_id'} command_json = encoded(semantic) command_hash = hashlib.sha256(command_json.encode()).hexdigest() raw_hash = hashlib.sha256(body).hexdigest() with self.connect() as db: db.execute('BEGIN IMMEDIATE') previous = db.execute('SELECT body_hash FROM inbox WHERE event_id=?', (event['event_id'],)).fetchone() if previous: if previous['body_hash'] != raw_hash: raise Refusal('EVENT_ID_CONFLICT') return {'accepted': True, 'duplicate': True} effect = db.execute('SELECT command_hash FROM effects WHERE effect_key=?', (effect_key,)).fetchone() if effect and effect['command_hash'] != command_hash: raise Refusal('EFFECT_CONFLICT') order = db.execute('SELECT * FROM orders WHERE order_key=?', (order_key,)).fetchone() if order and (order['offer_id'] != event['offer_id'] or order['email'] != event['customer']['email']): raise Refusal('ORDER_MAPPING_CONFLICT') db.execute('INSERT OR IGNORE INTO orders(order_key,offer_id,email) VALUES(?,?,?)', (order_key, event['offer_id'], event['customer']['email'])) if not effect: try: db.execute("INSERT INTO effects(effect_key,order_key,sequence,command_hash,command_json,state,created_at) VALUES(?,?,?,?,?,'WAITING',?)", (effect_key, order_key, event['sequence'], command_hash, command_json, int(time.time()))) except sqlite3.IntegrityError as error: raise Refusal('SEQUENCE_CONFLICT') from error db.execute('INSERT INTO inbox(event_id,body_hash,effect_key,received_at) VALUES(?,?,?,?)', (event['event_id'], raw_hash, effect_key, int(time.time()))) db.commit() return {'accepted': True, 'duplicate': effect is not None} def api(self, method, path, body=None, idempotency=None): headers = {'Authorization': 'Bearer ' + self.api_key, 'Accept': 'application/json', 'Content-Type': 'application/json', 'User-Agent': 'KDPika-Reference-Fulfillment/1'} if idempotency: headers['Idempotency-Key'] = idempotency request = urllib.request.Request(self.base + '/api/v1' + path, data=encoded(body).encode() if body is not None else None, headers=headers, method=method) opener = urllib.request.build_opener(NoRedirect()) try: response = opener.open(request, timeout=15) except urllib.error.HTTPError as error: response = error with response: raw = response.read(1048577) if len(raw) > 1048576: raise Refusal('UNEXPECTED_API_BODY') data = decoded(raw) if not isinstance(data, dict): raise Refusal('UNEXPECTED_API_BODY') return response.status, data def perform(self, command, order, effect_key): action = command['action'] if action == 'issue': if order['license_id']: raise Refusal('ORDER_ALREADY_HAS_LICENSE') offer = self.config['offers'][command['offer_id']] status, result = self.api('POST', '/licenses', {'product_id': self.config['product_id'], 'plan_id': offer['plan_id'], 'customer': command['customer']}, 'store_' + effect_key) item = result.get('license', {}) if status != 201 or result.get('success') is not True or not isinstance(item.get('id'), str) or not re.fullmatch(r'lic_[A-Za-z0-9_-]{1,100}', item['id']) or not isinstance(item.get('license_key'), str) or not 12 <= len(item['license_key']) <= 100 or item.get('environment') != self.environment: raise Refusal('ISSUANCE_RESULT_REQUIRES_REVIEW') return item['id'], item['license_key'], result.get('request_id') if not order['license_id']: raise Refusal('ISSUE_FIRST') license_id = order['license_id'] path = '/licenses/' + urllib.parse.quote(license_id, safe='') status, current = self.api('GET', path) item = current.get('license', {}) if status != 200 or current.get('success') is not True or item.get('id') != license_id or item.get('environment') != self.environment or item.get('status') != command['expected_status'] or item.get('expires_at') != command['expected_expires_at']: raise Refusal('LICENSE_CHANGED_RECONCILE_FIRST') if action == 'record_cancellation': # This records an authenticated decision; it does not cancel/refund a store payment or erase paid access. return license_id, None, current.get('request_id') body = {'days': self.config['offers'][command['offer_id']]['renewal_days']} if action == 'renew' else {'reason': 'Verified store fulfillment decision'} status, changed = self.api('POST', path + '/' + action, body) expected = {'suspend': 'suspended', 'restore': 'active', 'revoke': 'revoked', 'renew': 'active' if item['status'] == 'expired' else item['status']}[action] changed_item = changed.get('license', {}) if status != 200 or changed.get('success') is not True or changed_item.get('id') != license_id or changed_item.get('status') != expected or (action == 'renew' and not isinstance(changed_item.get('expires_at'), str)): raise Refusal('MUTATION_RESULT_REQUIRES_REVIEW') return license_id, None, changed.get('request_id') def work(self): lock_path = self.directory / 'worker.lock' if lock_path.is_symlink(): raise Refusal('INVALID_LOCK_FILE') with lock_path.open('a+b') as lock: try: fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) except BlockingIOError as error: raise Refusal('WORKER_BUSY') from error processed = 0 # One bounded cron run. Per-order sequence gaps wait; other independent orders can progress. while processed < 25: with self.connect() as db: db.execute('BEGIN IMMEDIATE') effect = db.execute("SELECT e.* FROM effects e JOIN orders o ON o.order_key=e.order_key WHERE e.state='WAITING' AND o.held=0 AND e.sequence=o.last_sequence+1 ORDER BY e.created_at,e.effect_key LIMIT 1").fetchone() if not effect: break order = db.execute('SELECT * FROM orders WHERE order_key=?', (effect['order_key'],)).fetchone() db.execute("UPDATE effects SET state='IN_FLIGHT' WHERE effect_key=?", (effect['effect_key'],)) db.execute('UPDATE orders SET held=1 WHERE order_key=?', (effect['order_key'],)) db.commit() try: license_id, key, request_id = self.perform(json.loads(effect['command_json']), order, effect['effect_key']) if request_id is not None and (not isinstance(request_id, str) or len(request_id) > 190): raise Refusal('UNEXPECTED_REQUEST_ID') with self.connect() as db: db.execute('BEGIN IMMEDIATE') if key: nonce = os.urandom(12) cipher = nonce + AESGCM(self.outbox_key).encrypt(nonce, key.encode(), effect['order_key'].encode()) db.execute('INSERT INTO outbox(order_key,license_id,cipher,created_at) VALUES(?,?,?,?)', (effect['order_key'], license_id, base64.b64encode(cipher).decode(), int(time.time()))) db.execute("UPDATE effects SET state='DONE',request_id=? WHERE effect_key=?", (request_id, effect['effect_key'])) db.execute('UPDATE orders SET license_id=?,last_sequence=?,held=0 WHERE order_key=?', (license_id, effect['sequence'], effect['order_key'])) db.commit() except Exception: with self.connect() as db: db.execute("UPDATE effects SET state='REVIEW',error_code='RECONCILE_BEFORE_RETRY' WHERE effect_key=?", (effect['effect_key'],)) # held=1 remains. A crash before this write leaves IN_FLIGHT, also held and never auto-retried. processed += 1 time.sleep(1.1) return {'processed': processed, 'states': self.inspect()} def inspect(self): with self.connect() as db: return {row['state']: row['count'] for row in db.execute('SELECT state,COUNT(*) AS count FROM effects GROUP BY state')} def export(self, order_id, path): if not ID.fullmatch(order_id): raise Refusal('INVALID_ORDER_ID') order_key = identity(self.config['source'], self.config['seller_id'], self.environment, order_id) with self.connect() as db: item = db.execute('SELECT b.*,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key WHERE b.order_key=?', (order_key,)).fetchone() if not item: raise Refusal('NO_SAVED_KEY') target = Path(path) if not target.is_absolute() or not target.parent.is_dir() or target.parent.stat().st_mode & 0o077: raise Refusal('PRIVATE_EXPORT_DIRECTORY_REQUIRED') raw = base64.b64decode(item['cipher'], validate=True) key = AESGCM(self.outbox_key).decrypt(raw[:12], raw[12:], order_key.encode()).decode() with target.open('x') as stream: target.chmod(0o600) json.dump({'license_id': item['license_id'], 'license_key': key, 'email': item['email']}, stream) return {'exported': True, 'contains_secret': True} class Receiver(BaseHTTPRequestHandler): def log_message(self, *args): return def reply(self, status, value): raw = encoded(value).encode() self.send_response(status) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(raw))) self.send_header('Cache-Control', 'no-store') self.send_header('X-Content-Type-Options', 'nosniff') self.end_headers() self.wfile.write(raw) def do_POST(self): self.connection.settimeout(5) if self.path != '/store-event': return self.reply(404, {'accepted': False}) length = self.headers.get_all('Content-Length', []) signatures = self.headers.get_all('X-Fulfillment-Signature', []) if self.headers.get('Transfer-Encoding') or len(length) != 1 or not length[0].isdigit(): return self.reply(400, {'accepted': False}) if int(length[0]) > LIMIT: return self.reply(413, {'accepted': False}) if len(signatures) != 1: return self.reply(401, {'accepted': False}) if self.headers.get('Content-Type', '').split(';')[0].strip().lower() != 'application/json': return self.reply(415, {'accepted': False}) try: raw = self.rfile.read(int(length[0])) if len(raw) != int(length[0]): return self.reply(400, {'accepted': False}) accepted = self.server.application.accept(raw, signatures[0]) return self.reply(202, accepted) except Refusal as error: return self.reply(401 if str(error) == 'INVALID_SIGNATURE' else 409, {'accepted': False}) except (ValueError, TypeError, KeyError): return self.reply(400, {'accepted': False}) except (OSError, sqlite3.Error): return self.reply(503, {'accepted': False}) def main(): try: app = Fulfillment() mode = sys.argv[1] if len(sys.argv) > 1 else '' if mode == 'serve' and len(sys.argv) == 2: port = int(os.environ.get('KDP_FULFILLMENT_PORT', '8112')) with HTTPServer(('127.0.0.1', port), Receiver) as server: server.application = app print('{"listening":true,"loopback_only":true}', flush=True) server.serve_forever() elif mode == 'work' and len(sys.argv) == 2: print(encoded(app.work())) elif mode == 'inspect' and len(sys.argv) == 2: print(encoded({'states': app.inspect()})) elif mode == 'export' and len(sys.argv) == 4: print(encoded(app.export(sys.argv[2], sys.argv[3]))) else: raise Refusal('USE_SERVE_WORK_INSPECT_OR_EXPORT') except Exception: print('{"completed":false,"error":"REVIEW_PRIVATE_CONFIGURATION_OR_STATE"}') return 1 return 0 if __name__ == '__main__': raise SystemExit(main()) ``` ## Expected result The actual local receiver and worker pass the shared lifecycle suite: durable receipt before work, authentic command acceptance, tamper/time/mode/mapping refusal, duplicate/conflicting effects, ordered gap handling, real creation and recipient [activation](/docs/v1/start/glossary#activation), renewal, suspension, restoration and terminal revocation. The suite also exports an encrypted saved key, refuses unsafe config/key drift, restarts the receiver, holds a simulated interrupted intent, and hides a genuinely successful remote create response behind a failing gateway. No second create is sent after that unknown result. These are exact-source local Sandbox tests on macOS. They are not an external store purchase, production deployment, native Windows test, email delivery test or promise of cross-system exactly-once behavior. The known API replay limitations remain unchanged. The read-before-write expected-state check is not an atomic remote compare-and-swap. ## Common mistakes - Sending a provider's native event directly here: this receiver accepts your verified adapter's private command format, not a Stripe/Paddle/PayPal callback. - Exposing the development server publicly without TLS, request bounds and rate protection. - Treating a 202 receipt or exported key file as proof of successful buyer email delivery. - Reusing a fresh state directory after an unknown create instead of reconciling the original order. ## Security notes - Keep config, state, encrypted-ledger backups and plaintext exports outside all web roots. - A backend that holds both the encryption key and database can decrypt its outbox; filesystem and process isolation still matter. - Keep one reviewed writer per order and preserve stable business-effect identity across delivery retries. - PHP SAPI can buffer bodies before script execution. Configure the ingress 256 KiB body limit and timeouts, not only application validation. - Recheck runtime support and dependency advisories before production deployment. The recorded advisory scan is not certification. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Safe configuration error | Missing/mismatched private mapping, key, permissions or runtime dependency | Verify private environment and required modules; never publish raw secrets to debug | | HTTP 401 | Wrong signature, timestamp, changed bytes or duplicate signature header | Sign exact bytes with the separate command secret and verify the clock | | HTTP 409 | Mapping/merchant/effect/sequence conflict | Reconcile trusted adapter state; do not invent a different effect ID | | No work processed | Already complete, sequence gap, held order or busy worker | Inspect saved state and authorized history before any write | | Export refused | Wrong order/key, unsafe target directory or file already exists | Verify recipient/order and use a new private owner-only target file | ## Next steps Review [the parent security checklist](/docs/v1/guides/any-store#security-notes), then test your real provider-specific adapter separately in its test environment. ## Related - [Generic store mapping and lifecycle](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Manual delivery and customer lookup](/docs/v1/guides/manual-issuance) # PHP store fulfillment reference Canonical: https://licensing.kdpika.com/docs/v1/guides/any-store/php --- title: "PHP store fulfillment reference" description: "Run the complete PHP trusted-command receiver and durable licensing worker with private state, encrypted key export and Sandbox evidence." slug: "/docs/v1/guides/any-store/php" section: "Webhooks and integrations" order: 783 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store","/docs/v1/api/idempotency"] draft: false --- Run the complete PHP reference after your trusted store adapter has independently verified the purchase and authorized one durable licensing effect. ## What you will learn - Install the exact reference dependencies without changing KDPika's application. - Start a loopback receiver and run its bounded worker separately. - Inspect durable outcomes and export a saved encrypted key for private [delivery](/docs/v1/start/glossary#delivery). ## Prerequisites - Read the complete [store backend protocol and mapping guide](/docs/v1/guides/any-store). - PHP 8.2+ with cURL, PDO SQLite, mbstring and OpenSSL supporting AES-256-GCM. The HTTP handler and CLI cron must use compatible PHP extensions and private state. - Use a POSIX macOS/Linux-oriented private filesystem, not a public download or temporary state directory. - Configure the private mapping and secret environment variables from the parent guide. - Use a [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product) and backend API key; do not set [Live](/docs/v1/start/glossary#live) confirmation for this test. ## Install and check 1. Download the [complete reference archive](/docs/downloads/store-fulfillment.zip). 2. Check its [SHA-256 source manifest](/docs/fulfillment-examples.json). 3. Extract the archive into a private backend project and enter its fulfillment directory. 4. Replace /private example paths with existing owner-only absolute paths for your backend. 5. Run the following installation/check commands. ```bash php --version php -m php -l php/server.php php -l send-command.php ``` Expected result: Both syntax checks exit 0. php -m lists curl, mbstring, openssl and pdo_sqlite. Confirm the web PHP handler has these extensions too; CLI output alone does not prove that. Keep credentials in private environment configuration, not in these commands, source files, terminal screenshots or a browser. This source package has no hidden service installation step and does not alter platform billing. ## Start the receiver ```bash php -S 127.0.0.1:8112 php/server.php ``` Expected result: the development receiver listens at HTTP 127.0.0.1:8112. It does not expose a public HTTPS service. For Node/Python/C#, KDP_FULFILLMENT_PORT selects another loopback port; PHP takes the port in its -S command. Keep this process running while sending a command from a separate terminal. Follow [send to the private receiver](/docs/v1/guides/any-store#send-to-the-private-receiver) with your verified private command file. The supplied sender requires PHP/cURL; it is not provider payment verification. A first valid receipt is accepted:true and duplicate:false. No [license](/docs/v1/start/glossary#license) is created until work runs. ## Run the worker ```bash php php/server.php work ``` Expected result for one new authorized issue: processed is 1 and states contains DONE:1. A completed unchanged replay processes 0 new effects. On uncertain/failed API outcomes, inspect REVIEW or IN_FLIGHT instead of assuming the operation did nothing. No full [license key](/docs/v1/start/glossary#license-key) is printed. The worker uses nonblocking POSIX flock on worker.lock. Another active worker is refused. An interrupted API call leaves its effect/order held; releasing the operating-system lock is not permission to retry the mutation. Each run handles at most 25 ready effects and pauses 1.1 seconds after an attempt. The durable state must persist across cron invocations and deployments. Do not mix language implementations on the same state directory or clear it when rotating the scoped API key. ## Inspect and export ```bash php php/server.php inspect ``` Expected result: states maps each existing effect state to its count. Inspection does not retry an API operation or print a complete key. ```bash php php/server.php export YOUR_ORDER_ID /private/key.json ``` Expected result after verified DONE issuance and with a new private target path: exported:true and contains_secret:true. The file contains license_id, license_key and email. Export refuses overwrite. This confirms a file was written, not that an email was sent. Authenticate the intended recipient and use your private delivery workflow; never place the export in public_html. ## Complete source The code below is included from the exact files executed by the Sandbox test. Common config.example.json, command.example.json, send-command.php and CONTRACT.md are in the same archive. The parent guide defines their required fields and security boundary in full. ```php $child)$item[$key]=$sort($child);} return $item; }; return json_encode($sort($value),JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRESERVE_ZERO_FRACTION|JSON_THROW_ON_ERROR); } function commandId(mixed $value): bool{return is_string($value)&&preg_match('/\A[A-Za-z0-9_.:-]{1,190}\z/D',$value)===1;} function commandIdentity(string ...$values): string{return hash('sha256',implode("\0",$values));} final class FulfillmentReference { public const LIMIT=262144; private PDO $db; private array $config; private string $directory; private string $secret; private string $outboxKey; private string $environment; private string $apiKey; private string $base; private bool $local; public function __construct() { umask(0077);$path=(string)getenv('KDP_FULFILLMENT_CONFIG'); if(!str_starts_with($path,'/'))throw new CommandRefusal('PRIVATE_ABSOLUTE_CONFIG_REQUIRED'); $this->config=json_decode((string)file_get_contents($path),true,64,JSON_THROW_ON_ERROR); $this->directory=rtrim((string)getenv('KDP_FULFILLMENT_STATE'),'/'); if(!str_starts_with($this->directory,'/')||is_link($this->directory))throw new CommandRefusal('PRIVATE_ABSOLUTE_STATE_REQUIRED'); if(!is_dir($this->directory))mkdir($this->directory,0700,true); if((fileperms($this->directory)&0077)!==0)throw new CommandRefusal('PRIVATE_STATE_PERMISSIONS_REQUIRED'); $this->secret=(string)getenv('KDP_FULFILLMENT_SECRET');$key=base64_decode((string)getenv('KDP_FULFILLMENT_OUTBOX_KEY'),true); if(strlen($this->secret)<32||$key===false||strlen($key)!==32)throw new CommandRefusal('PRIVATE_SECRETS_REQUIRED'); $this->outboxKey=$key;$this->environment=$this->config['environment']; if(!in_array($this->environment,['sandbox','live'],true))throw new CommandRefusal('INVALID_ENVIRONMENT'); if($this->environment==='live'&&getenv('KDP_CONFIRM_LIVE')!=='1')throw new CommandRefusal('LIVE_CONFIRMATION_REQUIRED'); foreach(['source','seller_id','product_id']as$field)if(!commandId($this->config[$field]??null))throw new CommandRefusal('INVALID_CONFIGURATION'); if(!is_array($this->config['offers']??null)||!$this->config['offers'])throw new CommandRefusal('OFFERS_REQUIRED'); foreach($this->config['offers']as$id=>$offer){if(!commandId($id)||!commandId($offer['plan_id']??null)||!is_int($offer['renewal_days']??null)||$offer['renewal_days']<1||$offer['renewal_days']>3650)throw new CommandRefusal('INVALID_OFFER_MAPPING');} $this->base=rtrim($this->config['base_url'],'/');$url=parse_url($this->base); $this->local=$this->environment==='sandbox'&&getenv('KDP_ALLOW_LOOPBACK')==='1'&&($url['scheme']??'')==='http'&&($url['host']??'')==='127.0.0.1'; if(!$url||empty($url['host'])||isset($url['user'])||isset($url['pass'])||isset($url['query'])||isset($url['fragment'])||!empty($url['path'])||(($url['scheme']??'')!=='https'&&!$this->local))throw new CommandRefusal('INVALID_BASE_URL'); $this->apiKey=(string)getenv('KDP_API_KEY');if(!str_starts_with($this->apiKey,'lmp_'.$this->environment.'_'))throw new CommandRefusal('API_KEY_ENVIRONMENT_MISMATCH'); $dbPath=$this->directory.'/fulfillment.sqlite';if(is_link($dbPath))throw new CommandRefusal('INVALID_STATE_FILE'); $this->db=new PDO('sqlite:'.$dbPath,null,null,[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC]); chmod($dbPath,0600);$this->db->exec('PRAGMA busy_timeout=3000'); $this->db->exec("CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,effect_key TEXT NOT NULL,received_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS orders(order_key TEXT PRIMARY KEY,offer_id TEXT NOT NULL,email TEXT NOT NULL,license_id TEXT,last_sequence INTEGER NOT NULL DEFAULT 0,held INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS effects(effect_key TEXT PRIMARY KEY,order_key TEXT NOT NULL,sequence INTEGER NOT NULL,command_hash TEXT NOT NULL,command_json TEXT NOT NULL,state TEXT NOT NULL,request_id TEXT,error_code TEXT,created_at INTEGER NOT NULL,UNIQUE(order_key,sequence)); CREATE TABLE IF NOT EXISTS outbox(order_key TEXT PRIMARY KEY,license_id TEXT NOT NULL,cipher TEXT NOT NULL,created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL,outbox_key_hash TEXT NOT NULL);"); $hash=hash('sha256',commandJson($this->config));$keyHash=hash('sha256',$this->outboxKey); $this->execute('INSERT OR IGNORE INTO binding(id,config_hash,outbox_key_hash) VALUES(1,?,?)',[$hash,$keyHash]); $binding=$this->one('SELECT * FROM binding WHERE id=1'); if($binding['config_hash']!==$hash||$binding['outbox_key_hash']!==$keyHash)throw new CommandRefusal('CONFIGURATION_CHANGED_RECONCILE_FIRST'); } private function execute(string $sql,array $values=[]): PDOStatement{$statement=$this->db->prepare($sql);$statement->execute($values);return $statement;} private function one(string $sql,array $values=[]): array|false{return $this->execute($sql,$values)->fetch();} private function transaction(callable $operation): mixed { $this->db->exec('BEGIN IMMEDIATE'); try{$result=$operation();$this->db->exec('COMMIT');return $result;}catch(Throwable $error){$this->db->exec('ROLLBACK');throw $error;} } private function validate(array $event): array { $fields=['version','source','seller_id','environment','event_id','effect_id','order_id','sequence','action','offer_id','customer','expected_status','expected_expires_at']; $keys=array_keys($event);sort($fields);sort($keys); if($keys!==$fields||($event['version']??null)!==1)throw new CommandRefusal('INVALID_COMMAND'); foreach(['source','seller_id','environment']as$field)if($event[$field]!==$this->config[$field])throw new CommandRefusal('WRONG_SELLER_OR_ENVIRONMENT'); foreach(['event_id','effect_id','order_id','offer_id']as$field)if(!commandId($event[$field]))throw new CommandRefusal('INVALID_IDENTIFIER'); if(!isset($this->config['offers'][$event['offer_id']])||!in_array($event['action'],['issue','renew','suspend','restore','revoke','record_cancellation'],true)||!is_int($event['sequence'])||$event['sequence']<1||$event['sequence']>2147483647)throw new CommandRefusal('UNSUPPORTED_COMMAND'); $customer=$event['customer'];$customerKeys=is_array($customer)?array_keys($customer):[];sort($customerKeys); if($customerKeys!==['email','name']||!is_string($customer['email'])||!filter_var($customer['email'],FILTER_VALIDATE_EMAIL)||strlen($customer['email'])>190||!is_string($customer['name'])||mb_strlen($customer['name'])>160)throw new CommandRefusal('INVALID_CUSTOMER'); if($event['expected_expires_at']!==null&&(!is_string($event['expected_expires_at'])||strlen($event['expected_expires_at'])>40))throw new CommandRefusal('INVALID_EXPECTED_EXPIRY'); if($event['action']==='issue'){ if($event['sequence']!==1||$event['expected_status']!==null||$event['expected_expires_at']!==null)throw new CommandRefusal('INVALID_ISSUANCE_COMMAND'); }elseif(!in_array($event['expected_status'],['active','suspended','revoked','expired','pending','cancelled'],true))throw new CommandRefusal('CURRENT_STATE_REQUIRED'); return $event; } public function accept(string $body,string $signature): array { if(strlen($body)>self::LIMIT||!preg_match('/\At=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})\z/D',$signature,$match)||abs(time()-(int)$match[1])>300||!hash_equals(hash_hmac('sha256',$match[1].'.'.$body,$this->secret),$match[2]))throw new CommandRefusal('INVALID_SIGNATURE'); $decoded=json_decode($body,false,64,JSON_THROW_ON_ERROR);if(!is_object($decoded))throw new CommandRefusal('INVALID_COMMAND'); $event=$this->validate(json_decode($body,true,64,JSON_THROW_ON_ERROR)); $orderKey=commandIdentity($event['source'],$event['seller_id'],$event['environment'],$event['order_id']); $effectKey=commandIdentity($event['source'],$event['seller_id'],$event['environment'],$event['effect_id']); $semantic=$event;unset($semantic['event_id']);$json=commandJson($semantic);$commandHash=hash('sha256',$json);$rawHash=hash('sha256',$body); return $this->transaction(function()use($event,$orderKey,$effectKey,$json,$commandHash,$rawHash):array{ $previous=$this->one('SELECT body_hash FROM inbox WHERE event_id=?',[$event['event_id']]); if($previous){if($previous['body_hash']!==$rawHash)throw new CommandRefusal('EVENT_ID_CONFLICT');return ['accepted'=>true,'duplicate'=>true];} $effect=$this->one('SELECT command_hash FROM effects WHERE effect_key=?',[$effectKey]); if($effect&&$effect['command_hash']!==$commandHash)throw new CommandRefusal('EFFECT_CONFLICT'); $order=$this->one('SELECT * FROM orders WHERE order_key=?',[$orderKey]); if($order&&($order['offer_id']!==$event['offer_id']||$order['email']!==$event['customer']['email']))throw new CommandRefusal('ORDER_MAPPING_CONFLICT'); $this->execute('INSERT OR IGNORE INTO orders(order_key,offer_id,email) VALUES(?,?,?)',[$orderKey,$event['offer_id'],$event['customer']['email']]); if(!$effect){ try{$this->execute("INSERT INTO effects(effect_key,order_key,sequence,command_hash,command_json,state,created_at) VALUES(?,?,?,?,?,'WAITING',?)",[$effectKey,$orderKey,$event['sequence'],$commandHash,$json,time()]);} catch(PDOException $error){if($error->getCode()==='23000')throw new CommandRefusal('SEQUENCE_CONFLICT');throw $error;} } $this->execute('INSERT INTO inbox(event_id,body_hash,effect_key,received_at) VALUES(?,?,?,?)',[$event['event_id'],$rawHash,$effectKey,time()]); return ['accepted'=>true,'duplicate'=>(bool)$effect]; }); } private function api(string $method,string $path,?array $body=null,?string $idempotency=null): array { $headers=['Authorization: Bearer '.$this->apiKey,'Accept: application/json','Content-Type: application/json','User-Agent: KDPika-Reference-Fulfillment/1']; if($idempotency!==null)$headers[]='Idempotency-Key: '.$idempotency; $raw='';$curl=curl_init($this->base.'/api/v1'.$path); $options=[CURLOPT_CUSTOMREQUEST=>$method,CURLOPT_HTTPHEADER=>$headers,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_CONNECTTIMEOUT=>5,CURLOPT_TIMEOUT=>15, CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2,CURLOPT_PROTOCOLS=>$this->local?CURLPROTO_HTTP:CURLPROTO_HTTPS, CURLOPT_WRITEFUNCTION=>static function($handle,string $bytes)use(&$raw):int{if(strlen($raw)+strlen($bytes)>1048576)return 0;$raw.=$bytes;return strlen($bytes);}]; if($body!==null)$options[CURLOPT_POSTFIELDS]=commandJson($body); curl_setopt_array($curl,$options);$sent=curl_exec($curl);$status=(int)curl_getinfo($curl,CURLINFO_RESPONSE_CODE); if($sent===false)throw new CommandRefusal('UNKNOWN_API_RESULT'); $data=json_decode($raw,true,64,JSON_THROW_ON_ERROR);if(!is_array($data))throw new CommandRefusal('UNEXPECTED_API_BODY');return [$status,$data]; } private function perform(array $command,array $order,string $effectKey): array { $action=$command['action']; if($action==='issue'){ if($order['license_id'])throw new CommandRefusal('ORDER_ALREADY_HAS_LICENSE'); $offer=$this->config['offers'][$command['offer_id']]; $idempotency=($this->config['create_idempotency']??'effect_hash')==='effect_id'?$command['effect_id']:'store_'.$effectKey; if(strlen($idempotency)<8||strlen($idempotency)>190)throw new CommandRefusal('INVALID_CREATE_IDEMPOTENCY'); [$status,$result]=$this->api('POST','/licenses',['product_id'=>$this->config['product_id'],'plan_id'=>$offer['plan_id'],'customer'=>$command['customer']],$idempotency); $item=$result['license']??[]; if($status!==201||($result['success']??null)!==true||!is_string($item['id']??null)||!preg_match('/\Alic_[A-Za-z0-9_-]{1,100}\z/D',$item['id'])||!is_string($item['license_key']??null)||strlen($item['license_key'])<12||strlen($item['license_key'])>100||($item['environment']??null)!==$this->environment)throw new CommandRefusal('ISSUANCE_RESULT_REQUIRES_REVIEW'); return [$item['id'],$item['license_key'],$result['request_id']??null]; } if(!$order['license_id'])throw new CommandRefusal('ISSUE_FIRST'); $id=$order['license_id'];$path='/licenses/'.rawurlencode($id);[$status,$current]=$this->api('GET',$path);$item=$current['license']??[]; if($status!==200||($current['success']??null)!==true||($item['id']??null)!==$id||($item['environment']??null)!==$this->environment||($item['status']??null)!==$command['expected_status']||($item['expires_at']??null)!==$command['expected_expires_at'])throw new CommandRefusal('LICENSE_CHANGED_RECONCILE_FIRST'); if($action==='record_cancellation')return [$id,null,$current['request_id']??null]; $body=$action==='renew'?['days'=>$this->config['offers'][$command['offer_id']]['renewal_days']]:['reason'=>'Verified store fulfillment decision']; [$status,$changed]=$this->api('POST',$path.'/'.$action,$body);$value=$changed['license']??[]; $expected=match($action){'suspend'=>'suspended','restore'=>'active','revoke'=>'revoked','renew'=>$item['status']==='expired'?'active':$item['status']}; if($status!==200||($changed['success']??null)!==true||($value['id']??null)!==$id||($value['status']??null)!==$expected||($action==='renew'&&!is_string($value['expires_at']??null)))throw new CommandRefusal('MUTATION_RESULT_REQUIRES_REVIEW'); return [$id,null,$changed['request_id']??null]; } public function work(): array { $path=$this->directory.'/worker.lock';if(is_link($path))throw new CommandRefusal('INVALID_LOCK_FILE'); $lock=fopen($path,'c');if(!$lock||!flock($lock,LOCK_EX|LOCK_NB))throw new CommandRefusal('WORKER_BUSY'); $processed=0; try{ while($processed<25){ $claimed=$this->transaction(function():?array{ $effect=$this->one("SELECT e.* FROM effects e JOIN orders o ON o.order_key=e.order_key WHERE e.state='WAITING' AND o.held=0 AND e.sequence=o.last_sequence+1 ORDER BY e.created_at,e.effect_key LIMIT 1"); if(!$effect)return null;$order=$this->one('SELECT * FROM orders WHERE order_key=?',[$effect['order_key']]); $this->execute("UPDATE effects SET state='IN_FLIGHT' WHERE effect_key=?",[$effect['effect_key']]);$this->execute('UPDATE orders SET held=1 WHERE order_key=?',[$effect['order_key']]); return [$effect,$order]; }); if($claimed===null)break;[$effect,$order]=$claimed; try{ [$id,$key,$requestId]=$this->perform(json_decode($effect['command_json'],true,64,JSON_THROW_ON_ERROR),$order,$effect['effect_key']); if($requestId!==null&&(!is_string($requestId)||strlen($requestId)>190))throw new CommandRefusal('UNEXPECTED_REQUEST_ID'); $this->transaction(function()use($effect,$id,$key,$requestId):void{ if($key!==null){ $nonce=random_bytes(12);$tag='';$cipher=openssl_encrypt($key,'aes-256-gcm',$this->outboxKey,OPENSSL_RAW_DATA,$nonce,$tag,$effect['order_key'],16); if($cipher===false)throw new CommandRefusal('OUTBOX_ENCRYPTION_FAILED'); $this->execute('INSERT INTO outbox(order_key,license_id,cipher,created_at) VALUES(?,?,?,?)',[$effect['order_key'],$id,base64_encode($nonce.$cipher.$tag),time()]); } $this->execute("UPDATE effects SET state='DONE',request_id=? WHERE effect_key=?",[$requestId,$effect['effect_key']]); $this->execute('UPDATE orders SET license_id=?,last_sequence=?,held=0 WHERE order_key=?',[$id,$effect['sequence'],$effect['order_key']]); }); }catch(Throwable){$this->execute("UPDATE effects SET state='REVIEW',error_code='RECONCILE_BEFORE_RETRY' WHERE effect_key=?",[$effect['effect_key']]);} $processed++;usleep(1100000); } }finally{flock($lock,LOCK_UN);fclose($lock);} return ['processed'=>$processed,'states'=>$this->inspect()]; } public function inspect(): array { $states=[];foreach($this->execute('SELECT state,COUNT(*) AS count FROM effects GROUP BY state')->fetchAll()as$row)$states[$row['state']]=(int)$row['count'];return $states; } public function export(string $orderId,string $target): array { if(!commandId($orderId))throw new CommandRefusal('INVALID_ORDER_ID'); $orderKey=commandIdentity($this->config['source'],$this->config['seller_id'],$this->environment,$orderId); $row=$this->one('SELECT b.*,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key WHERE b.order_key=?',[$orderKey]);if(!$row)throw new CommandRefusal('NO_SAVED_KEY'); $parent=dirname($target);if(!str_starts_with($target,'/')||!is_dir($parent)||(fileperms($parent)&0077)!==0)throw new CommandRefusal('PRIVATE_EXPORT_DIRECTORY_REQUIRED'); $raw=base64_decode($row['cipher'],true);if($raw===false||strlen($raw)<29)throw new CommandRefusal('INVALID_OUTBOX'); $key=openssl_decrypt(substr($raw,12,-16),'aes-256-gcm',$this->outboxKey,OPENSSL_RAW_DATA,substr($raw,0,12),substr($raw,-16),$orderKey); if($key===false)throw new CommandRefusal('OUTBOX_DECRYPTION_FAILED'); $stream=fopen($target,'xb');if(!$stream)throw new CommandRefusal('EXPORT_EXISTS'); try{chmod($target,0600);$json=commandJson(['license_id'=>$row['license_id'],'license_key'=>$key,'email'=>$row['email']]);if(fwrite($stream,$json)!==strlen($json))throw new CommandRefusal('EXPORT_WRITE_FAILED');}finally{fclose($stream);} return ['exported'=>true,'contains_secret'=>true]; } } function referenceReply(int $status,array $value): never { http_response_code($status);header('Content-Type: application/json');header('Cache-Control: no-store');header('X-Content-Type-Options: nosniff');echo commandJson($value);exit; } if(defined('KDP_FULFILLMENT_LIBRARY_ONLY')&&KDP_FULFILLMENT_LIBRARY_ONLY===true)return; try{ $app=new FulfillmentReference(); if(PHP_SAPI==='cli'){ if(($argc??0)===2&&$argv[1]==='work')$output=$app->work(); elseif(($argc??0)===2&&$argv[1]==='inspect')$output=['states'=>$app->inspect()]; elseif(($argc??0)===4&&$argv[1]==='export')$output=$app->export($argv[2],$argv[3]); else throw new CommandRefusal('USE_WORK_INSPECT_OR_EXPORT'); echo commandJson($output).PHP_EOL;exit; } if(($_SERVER['REQUEST_METHOD']??'')!=='POST'||($_SERVER['REQUEST_URI']??'')!=='/store-event')referenceReply(404,['accepted'=>false]); $length=$_SERVER['CONTENT_LENGTH']??''; if(!ctype_digit((string)$length)||isset($_SERVER['HTTP_TRANSFER_ENCODING']))referenceReply(400,['accepted'=>false]); if((int)$length>FulfillmentReference::LIMIT)referenceReply(413,['accepted'=>false]); if(strtolower(trim(explode(';',$_SERVER['CONTENT_TYPE']??'')[0]))!=='application/json')referenceReply(415,['accepted'=>false]); $signature=$_SERVER['HTTP_X_FULFILLMENT_SIGNATURE']??''; if(!is_string($signature)||!preg_match('/\At=[1-9][0-9]{0,10},v1=[0-9a-f]{64}\z/D',$signature))referenceReply(401,['accepted'=>false]); $body=file_get_contents('php://input',false,null,0,FulfillmentReference::LIMIT+1); if($body===false||strlen($body)!==(int)$length)referenceReply(400,['accepted'=>false]); referenceReply(202,$app->accept($body,$signature)); }catch(Throwable $error){ if(PHP_SAPI==='cli'){echo '{"completed":false,"error":"REVIEW_PRIVATE_CONFIGURATION_OR_STATE"}'.PHP_EOL;exit(1);} $status=$error instanceof CommandRefusal?($error->getMessage()==='INVALID_SIGNATURE'?401:409):($error instanceof JsonException?400:503); referenceReply($status,['accepted'=>false]); } ``` ## Expected result The actual local receiver and worker pass the shared lifecycle suite: durable receipt before work, authentic command acceptance, tamper/time/mode/mapping refusal, duplicate/conflicting effects, ordered gap handling, real creation and recipient [activation](/docs/v1/start/glossary#activation), renewal, suspension, restoration and terminal revocation. The suite also exports an encrypted saved key, refuses unsafe config/key drift, restarts the receiver, holds a simulated interrupted intent, and hides a genuinely successful remote create response behind a failing gateway. No second create is sent after that unknown result. These are exact-source local Sandbox tests on macOS. They are not an external store purchase, production deployment, native Windows test, email delivery test or promise of cross-system exactly-once behavior. The known API replay limitations remain unchanged. The read-before-write expected-state check is not an atomic remote compare-and-swap. ## Common mistakes - Sending a provider's native event directly here: this receiver accepts your verified adapter's private command format, not a Stripe/Paddle/PayPal callback. - Exposing the development server publicly without TLS, request bounds and rate protection. - Treating a 202 receipt or exported key file as proof of successful buyer email delivery. - Reusing a fresh state directory after an unknown create instead of reconciling the original order. ## Security notes - Keep config, state, encrypted-ledger backups and plaintext exports outside all web roots. - A backend that holds both the encryption key and database can decrypt its outbox; filesystem and process isolation still matter. - Keep one reviewed writer per order and preserve stable business-effect identity across delivery retries. - PHP SAPI can buffer bodies before script execution. Configure the ingress 256 KiB body limit and timeouts, not only application validation. - Recheck runtime support and dependency advisories before production deployment. The recorded advisory scan is not certification. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Safe configuration error | Missing/mismatched private mapping, key, permissions or runtime dependency | Verify private environment and required modules; never publish raw secrets to debug | | HTTP 401 | Wrong signature, timestamp, changed bytes or duplicate signature header | Sign exact bytes with the separate command secret and verify the clock | | HTTP 409 | Mapping/merchant/effect/sequence conflict | Reconcile trusted adapter state; do not invent a different effect ID | | No work processed | Already complete, sequence gap, held order or busy worker | Inspect saved state and authorized history before any write | | Export refused | Wrong order/key, unsafe target directory or file already exists | Verify recipient/order and use a new private owner-only target file | ## Next steps Review [the parent security checklist](/docs/v1/guides/any-store#security-notes), then test your real provider-specific adapter separately in its test environment. ## Related - [Generic store mapping and lifecycle](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Manual delivery and customer lookup](/docs/v1/guides/manual-issuance) # C# store fulfillment reference Canonical: https://licensing.kdpika.com/docs/v1/guides/any-store/csharp --- title: "C# store fulfillment reference" description: "Run the complete C# trusted-command receiver and durable licensing worker with private state, encrypted key export and Sandbox evidence." slug: "/docs/v1/guides/any-store/csharp" section: "Webhooks and integrations" order: 784 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store","/docs/v1/api/idempotency"] draft: false --- Run the complete C# reference after your trusted store adapter has independently verified the purchase and authorized one durable licensing effect. ## What you will learn - Install the exact reference dependencies without changing KDPika's application. - Start a loopback receiver and run its bounded worker separately. - Inspect durable outcomes and export a saved encrypted key for private [delivery](/docs/v1/start/glossary#delivery). ## Prerequisites - Read the complete [store backend protocol and mapping guide](/docs/v1/guides/any-store). - .NET 10 SDK/runtime, POSIX libc flock and Microsoft.Data.Sqlite 10.0.13 with the supplied transitive lock. This reference explicitly refuses native Windows. - Use a POSIX macOS/Linux-oriented private filesystem, not a public download or temporary state directory. - Configure the private mapping and secret environment variables from the parent guide. - Use a [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product) and backend API key; do not set [Live](/docs/v1/start/glossary#live) confirmation for this test. ## Install and check 1. Download the [complete reference archive](/docs/downloads/store-fulfillment.zip). 2. Check its [SHA-256 source manifest](/docs/fulfillment-examples.json). 3. Extract the archive into a private backend project and enter its fulfillment directory. 4. Replace /private example paths with existing owner-only absolute paths for your backend. 5. Run the following installation/check commands. ```bash dotnet --version dotnet restore csharp/Fulfillment.csproj --locked-mode dotnet build csharp/Fulfillment.csproj --no-restore --configuration Release --output /private/kdpika-fulfillment-build ``` Expected result: Restore honors packages.lock.json; the build exits 0. The tested .NET 10 build has zero warnings and errors. The archive contains source and lock metadata, not a prebuilt host-specific binary. Keep credentials in private environment configuration, not in these commands, source files, terminal screenshots or a browser. This source package has no hidden service installation step and does not alter platform billing. ## Start the receiver ```bash dotnet /private/kdpika-fulfillment-build/Fulfillment.dll serve ``` Expected result: the development receiver listens at HTTP 127.0.0.1:8112. It does not expose a public HTTPS service. For Node/Python/C#, KDP_FULFILLMENT_PORT selects another loopback port; PHP takes the port in its -S command. Keep this process running while sending a command from a separate terminal. Follow [send to the private receiver](/docs/v1/guides/any-store#send-to-the-private-receiver) with your verified private command file. The supplied sender requires PHP/cURL; it is not provider payment verification. A first valid receipt is accepted:true and duplicate:false. No [license](/docs/v1/start/glossary#license) is created until work runs. ## Run the worker ```bash dotnet /private/kdpika-fulfillment-build/Fulfillment.dll work ``` Expected result for one new authorized issue: processed is 1 and states contains DONE:1. A completed unchanged replay processes 0 new effects. On uncertain/failed API outcomes, inspect REVIEW or IN_FLIGHT instead of assuming the operation did nothing. No full [license key](/docs/v1/start/glossary#license-key) is printed. The worker uses libc flock through a small POSIX interop boundary. It refuses a competing worker. A crashed API attempt remains IN_FLIGHT/held and is never automatically repeated. The explicit POSIX restriction avoids pretending that native Windows ACLs/locking were tested. Each run handles at most 25 ready effects and pauses 1.1 seconds after an attempt. The durable state must persist across cron invocations and deployments. Do not mix language implementations on the same state directory or clear it when rotating the scoped API key. ## Inspect and export ```bash dotnet /private/kdpika-fulfillment-build/Fulfillment.dll inspect ``` Expected result: states maps each existing effect state to its count. Inspection does not retry an API operation or print a complete key. ```bash dotnet /private/kdpika-fulfillment-build/Fulfillment.dll export YOUR_ORDER_ID /private/key.json ``` Expected result after verified DONE issuance and with a new private target path: exported:true and contains_secret:true. The file contains license_id, license_key and email. Export refuses overwrite. This confirms a file was written, not that an email was sent. Authenticate the intended recipient and use your private delivery workflow; never place the export in public_html. ## Complete source The code below is included from the exact files executed by the Sandbox test. Common config.example.json, command.example.json, send-command.php and CONTRACT.md are in the same archive. The parent guide defines their required fields and security boundary in full. ```xml net10.0 enable enable true ``` ```csharp // Trusted fulfillment receiver/worker; not a payment gateway or browser client. // .NET 10, Microsoft.Data.Sqlite 10.0.13, POSIX flock and private SQLite/AES-GCM. // Set private KDP_FULFILLMENT_CONFIG/STATE/SECRET/OUTBOX_KEY and KDP_API_KEY. // dotnet Fulfillment.dll serve | work | inspect | export YOUR_ORDER_ID /private/key.json // Expected: safe receipt/work JSON, full keys only in explicit private exports. using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.RegularExpressions; using Microsoft.Data.Sqlite; static class Program { [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] public static async Task Main(string[] args) { try { if (!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) throw new PlatformNotSupportedException("POSIX_REFERENCE_REQUIRED"); using var app = new Fulfillment(); object result; if (args is ["serve"]) { await app.Serve(); return 0; } if (args is ["work"]) result = await app.Work(); else if (args is ["inspect"]) result = new { states = app.Inspect() }; else if (args is ["export", var order, var target]) result = app.Export(order, target); else throw new Refusal("USE_SERVE_WORK_INSPECT_OR_EXPORT"); Console.WriteLine(JsonSerializer.Serialize(result, Fulfillment.JsonOptions)); return 0; } catch { Console.WriteLine("{\"completed\":false,\"error\":\"REVIEW_PRIVATE_CONFIGURATION_OR_STATE\"}"); return 1; } } } sealed class Refusal(string code) : Exception(code) { } [SupportedOSPlatform("linux")] [SupportedOSPlatform("macos")] sealed class Fulfillment : IDisposable { public const int Limit = 262144; public static readonly JsonSerializerOptions JsonOptions = new() { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; static readonly UTF8Encoding Utf8 = new(false, true); static readonly Regex Identifier = new(@"\A[A-Za-z0-9_.:-]{1,190}\z", RegexOptions.CultureInvariant); readonly SqliteConnection db; readonly JsonObject config; readonly string directory, secret, environment, apiKey, baseUrl; readonly byte[] outboxKey; readonly object receiverLock = new(); readonly HttpClient http = new(new SocketsHttpHandler { AllowAutoRedirect = false, ConnectTimeout = TimeSpan.FromSeconds(5) }) { Timeout = TimeSpan.FromSeconds(15) }; [DllImport("libc", SetLastError = true)] static extern uint umask(uint mask); [DllImport("libc", SetLastError = true)] static extern int flock(int descriptor, int operation); static string Env(string name) => Environment.GetEnvironmentVariable(name) ?? ""; static string Hash(byte[] value) => Convert.ToHexString(SHA256.HashData(value)).ToLowerInvariant(); static string Hash(string value) => Hash(Utf8.GetBytes(value)); static string Identity(params string[] values) => Hash(string.Join('\0', values)); static string Text(JsonNode? value) => value?.GetValue() ?? throw new Refusal("STRING_REQUIRED"); static bool IsId(string value) => Identifier.IsMatch(value); static long Now() => DateTimeOffset.UtcNow.ToUnixTimeSeconds(); static string? StringValue(Dictionary item, string key) => item[key] as string; static string Encode(JsonNode? node) { JsonNode? Sort(JsonNode? item) => item switch { JsonObject obj => new JsonObject(obj.OrderBy(pair => pair.Key, StringComparer.Ordinal).Select(pair => new KeyValuePair(pair.Key, Sort(pair.Value)))), JsonArray list => new JsonArray(list.Select(Sort).ToArray()), _ => item?.DeepClone() }; return Sort(node)?.ToJsonString(JsonOptions) ?? "null"; } static void Require(bool condition, string code) { if (!condition) throw new Refusal(code); } static void PrivateDirectory(string value) { Require(Path.IsPathFullyQualified(value), "PRIVATE_ABSOLUTE_DIRECTORY_REQUIRED"); if (Directory.Exists(value)) Require(new DirectoryInfo(value).LinkTarget is null, "SYMLINK_DIRECTORY_REFUSED"); Directory.CreateDirectory(value, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); Require(((int)File.GetUnixFileMode(value) & 0x3F) == 0, "PRIVATE_DIRECTORY_PERMISSIONS_REQUIRED"); } public Fulfillment() { Require(OperatingSystem.IsLinux() || OperatingSystem.IsMacOS(), "POSIX_REFERENCE_REQUIRED"); umask(0x3F); var configFile = Env("KDP_FULFILLMENT_CONFIG"); Require(Path.IsPathFullyQualified(configFile), "PRIVATE_ABSOLUTE_CONFIG_REQUIRED"); config = JsonNode.Parse(File.ReadAllBytes(configFile)) as JsonObject ?? throw new Refusal("INVALID_CONFIG"); directory = Env("KDP_FULFILLMENT_STATE"); PrivateDirectory(directory); secret = Env("KDP_FULFILLMENT_SECRET"); outboxKey = Convert.FromBase64String(Env("KDP_FULFILLMENT_OUTBOX_KEY")); Require(secret.Length >= 32 && outboxKey.Length == 32, "PRIVATE_SECRETS_REQUIRED"); environment = Text(config["environment"]); Require(environment is "sandbox" or "live", "INVALID_ENVIRONMENT"); Require(environment != "live" || Env("KDP_CONFIRM_LIVE") == "1", "LIVE_CONFIRMATION_REQUIRED"); foreach (var name in new[] { "source", "seller_id", "product_id" }) Require(IsId(Text(config[name])), "INVALID_CONFIGURATION"); Require(config["offers"] is JsonObject { Count: > 0 }, "OFFERS_REQUIRED"); foreach (var offer in (JsonObject)config["offers"]!) { Require(IsId(offer.Key) && IsId(Text(offer.Value?["plan_id"])), "INVALID_OFFER_MAPPING"); var days = offer.Value?["renewal_days"]?.GetValue() ?? 0; Require(days is >= 1 and <= 3650, "INVALID_OFFER_MAPPING"); } baseUrl = Text(config["base_url"]).TrimEnd('/'); var url = new Uri(baseUrl, UriKind.Absolute); var local = environment == "sandbox" && Env("KDP_ALLOW_LOOPBACK") == "1" && url.Scheme == "http" && url.Host == "127.0.0.1"; Require(url.UserInfo == "" && url.Query == "" && url.Fragment == "" && url.AbsolutePath == "/" && url.Host != "" && (url.Scheme == "https" || local), "INVALID_BASE_URL"); apiKey = Env("KDP_API_KEY"); Require(apiKey.StartsWith("lmp_" + environment + "_", StringComparison.Ordinal), "API_KEY_ENVIRONMENT_MISMATCH"); var database = Path.Combine(directory, "fulfillment.sqlite"); Require(new FileInfo(database).LinkTarget is null, "INVALID_STATE_FILE"); db = new SqliteConnection(new SqliteConnectionStringBuilder { DataSource = database, Pooling = false, DefaultTimeout = 3 }.ToString()); db.Open(); File.SetUnixFileMode(database, UnixFileMode.UserRead | UnixFileMode.UserWrite); Run(""" PRAGMA busy_timeout=3000; CREATE TABLE IF NOT EXISTS inbox(event_id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,effect_key TEXT NOT NULL,received_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS orders(order_key TEXT PRIMARY KEY,offer_id TEXT NOT NULL,email TEXT NOT NULL,license_id TEXT,last_sequence INTEGER NOT NULL DEFAULT 0,held INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS effects(effect_key TEXT PRIMARY KEY,order_key TEXT NOT NULL,sequence INTEGER NOT NULL,command_hash TEXT NOT NULL,command_json TEXT NOT NULL,state TEXT NOT NULL,request_id TEXT,error_code TEXT,created_at INTEGER NOT NULL,UNIQUE(order_key,sequence)); CREATE TABLE IF NOT EXISTS outbox(order_key TEXT PRIMARY KEY,license_id TEXT NOT NULL,cipher TEXT NOT NULL,created_at INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL,outbox_key_hash TEXT NOT NULL); """); var configHash = Hash(Encode(config)); var keyHash = Hash(outboxKey); Run("INSERT OR IGNORE INTO binding(id,config_hash,outbox_key_hash) VALUES(1,@p0,@p1)", configHash, keyHash); var bound = One("SELECT * FROM binding WHERE id=1")!; Require(StringValue(bound, "config_hash") == configHash && StringValue(bound, "outbox_key_hash") == keyHash, "CONFIGURATION_CHANGED_RECONCILE_FIRST"); } SqliteCommand Command(string sql, params object?[] values) { var command = db.CreateCommand(); command.CommandText = sql; for (var i = 0; i < values.Length; i++) command.Parameters.AddWithValue("@p" + i, values[i] ?? DBNull.Value); return command; } void Run(string sql, params object?[] values) { using var command = Command(sql, values); command.ExecuteNonQuery(); } Dictionary? One(string sql, params object?[] values) { using var command = Command(sql, values); using var reader = command.ExecuteReader(); if (!reader.Read()) return null; return Enumerable.Range(0, reader.FieldCount).ToDictionary(reader.GetName, index => reader.IsDBNull(index) ? null : reader.GetValue(index)); } T Transaction(Func operation) { Run("BEGIN IMMEDIATE"); try { var result = operation(); Run("COMMIT"); return result; } catch { Run("ROLLBACK"); throw; } } JsonObject Validate(JsonObject command) { var fields = new[] { "version", "source", "seller_id", "environment", "event_id", "effect_id", "order_id", "sequence", "action", "offer_id", "customer", "expected_status", "expected_expires_at" }; Require(command.Select(pair => pair.Key).Order().SequenceEqual(fields.Order()) && command["version"]?.GetValue() == 1, "INVALID_COMMAND"); foreach (var field in new[] { "source", "seller_id", "environment" }) Require(Text(command[field]) == Text(config[field]), "WRONG_SELLER_OR_ENVIRONMENT"); foreach (var field in new[] { "event_id", "effect_id", "order_id", "offer_id" }) Require(IsId(Text(command[field])), "INVALID_IDENTIFIER"); Require(((JsonObject)config["offers"]!).ContainsKey(Text(command["offer_id"])), "UNMAPPED_OFFER"); var action = Text(command["action"]); var sequence = command["sequence"]?.GetValue() ?? 0; Require(sequence >= 1 && new[] { "issue", "renew", "suspend", "restore", "revoke", "record_cancellation" }.Contains(action), "UNSUPPORTED_COMMAND"); Require(command["customer"] is JsonObject customer && customer.Select(pair => pair.Key).Order().SequenceEqual(new[] { "email", "name" }), "INVALID_CUSTOMER"); var email = Text(command["customer"]?["email"]); var name = Text(command["customer"]?["name"]); Require(email.Length <= 190 && Regex.IsMatch(email, @"\A[^\s@]{1,120}@[^\s@]{1,120}\z") && name.EnumerateRunes().Count() <= 160, "INVALID_CUSTOMER"); Require(command["expected_expires_at"] is null || Text(command["expected_expires_at"]).Length <= 40, "INVALID_EXPECTED_EXPIRY"); if (action == "issue") Require(sequence == 1 && command["expected_status"] is null && command["expected_expires_at"] is null, "INVALID_ISSUANCE_COMMAND"); else Require(new[] { "active", "suspended", "revoked", "expired", "pending", "cancelled" }.Contains(Text(command["expected_status"])), "CURRENT_STATE_REQUIRED"); return command; } public object Accept(byte[] body, string signature) { var match = Regex.Match(signature, @"\At=([1-9][0-9]{0,10}),v1=([0-9a-f]{64})\z"); Require(body.Length <= Limit && match.Success && Math.Abs(Now() - long.Parse(match.Groups[1].Value)) <= 300, "INVALID_SIGNATURE"); var message = Utf8.GetBytes(match.Groups[1].Value + ".").Concat(body).ToArray(); Require(CryptographicOperations.FixedTimeEquals(HMACSHA256.HashData(Utf8.GetBytes(secret), message), Convert.FromHexString(match.Groups[2].Value)), "INVALID_SIGNATURE"); var command = Validate(JsonNode.Parse(Utf8.GetString(body)) as JsonObject ?? throw new Refusal("INVALID_COMMAND")); var orderKey = Identity(Text(command["source"]), Text(command["seller_id"]), environment, Text(command["order_id"])); var effectKey = Identity(Text(command["source"]), Text(command["seller_id"]), environment, Text(command["effect_id"])); var semantic = (JsonObject)command.DeepClone(); semantic.Remove("event_id"); var json = Encode(semantic); var commandHash = Hash(json); var rawHash = Hash(body); return Transaction(() => { var previous = One("SELECT body_hash FROM inbox WHERE event_id=@p0", Text(command["event_id"])); if (previous is not null) { Require(StringValue(previous, "body_hash") == rawHash, "EVENT_ID_CONFLICT"); return new { accepted = true, duplicate = true }; } var effect = One("SELECT command_hash FROM effects WHERE effect_key=@p0", effectKey); Require(effect is null || StringValue(effect, "command_hash") == commandHash, "EFFECT_CONFLICT"); var order = One("SELECT * FROM orders WHERE order_key=@p0", orderKey); var offer = Text(command["offer_id"]); var email = Text(command["customer"]?["email"]); Require(order is null || (StringValue(order, "offer_id") == offer && StringValue(order, "email") == email), "ORDER_MAPPING_CONFLICT"); Run("INSERT OR IGNORE INTO orders(order_key,offer_id,email) VALUES(@p0,@p1,@p2)", orderKey, offer, email); if (effect is null) { var sequence = command["sequence"]!.GetValue(); Require(One("SELECT effect_key FROM effects WHERE order_key=@p0 AND sequence=@p1", orderKey, sequence) is null, "SEQUENCE_CONFLICT"); Run("INSERT INTO effects(effect_key,order_key,sequence,command_hash,command_json,state,created_at) VALUES(@p0,@p1,@p2,@p3,@p4,'WAITING',@p5)", effectKey, orderKey, sequence, commandHash, json, Now()); } Run("INSERT INTO inbox(event_id,body_hash,effect_key,received_at) VALUES(@p0,@p1,@p2,@p3)", Text(command["event_id"]), rawHash, effectKey, Now()); return new { accepted = true, duplicate = effect is not null }; }); } async Task<(int, JsonObject)> Api(HttpMethod method, string route, JsonObject? body = null, string? idempotency = null) { using var request = new HttpRequestMessage(method, baseUrl + "/api/v1" + route); request.Headers.Authorization = new("Bearer", apiKey); request.Headers.Accept.ParseAdd("application/json"); request.Headers.UserAgent.ParseAdd("KDPika-Reference-Fulfillment/1"); if (idempotency is not null) request.Headers.Add("Idempotency-Key", idempotency); if (body is not null) request.Content = new StringContent(Encode(body), Utf8, "application/json"); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)); using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeout.Token); using var input = await response.Content.ReadAsStreamAsync(timeout.Token); using var output = new MemoryStream(); var buffer = new byte[8192]; int count; while ((count = await input.ReadAsync(buffer, timeout.Token)) > 0) { Require(output.Length + count <= 1048576, "UNEXPECTED_API_BODY"); output.Write(buffer, 0, count); } return ((int)response.StatusCode, JsonNode.Parse(output.ToArray()) as JsonObject ?? throw new Refusal("UNEXPECTED_API_BODY")); } async Task<(string Id, string? Key, string? RequestId)> Perform(JsonObject command, Dictionary order, string effectKey) { var action = Text(command["action"]); if (action == "issue") { Require(order["license_id"] is null, "ORDER_ALREADY_HAS_LICENSE"); var offer = config["offers"]![Text(command["offer_id"])]; var (status, result) = await Api(HttpMethod.Post, "/licenses", new JsonObject { ["product_id"] = Text(config["product_id"]), ["plan_id"] = Text(offer?["plan_id"]), ["customer"] = command["customer"]!.DeepClone() }, "store_" + effectKey); var item = result["license"]; Require(status == 201 && result["success"]?.GetValue() == true && item is JsonObject && Regex.IsMatch(Text(item["id"]), @"\Alic_[A-Za-z0-9_-]{1,100}\z") && Text(item["license_key"]).Length is >= 12 and <= 100 && Text(item["environment"]) == environment, "ISSUANCE_RESULT_REQUIRES_REVIEW"); return (Text(item!["id"]), Text(item["license_key"]), result["request_id"]?.GetValue()); } var licenseId = StringValue(order, "license_id") ?? throw new Refusal("ISSUE_FIRST"); var route = "/licenses/" + Uri.EscapeDataString(licenseId); var (code, current) = await Api(HttpMethod.Get, route); var license = current["license"]; Require(code == 200 && current["success"]?.GetValue() == true && license is JsonObject && Text(license["id"]) == licenseId && Text(license["environment"]) == environment && JsonNode.DeepEquals(license["status"], command["expected_status"]) && JsonNode.DeepEquals(license["expires_at"], command["expected_expires_at"]), "LICENSE_CHANGED_RECONCILE_FIRST"); if (action == "record_cancellation") return (licenseId, null, current["request_id"]?.GetValue()); var body = action == "renew" ? new JsonObject { ["days"] = config["offers"]![Text(command["offer_id"])]!["renewal_days"]!.DeepClone() } : new JsonObject { ["reason"] = "Verified store fulfillment decision" }; var (changedCode, changed) = await Api(HttpMethod.Post, route + "/" + action, body); var expected = action switch { "suspend" => "suspended", "restore" => "active", "revoke" => "revoked", "renew" => Text(license!["status"]) == "expired" ? "active" : Text(license["status"]), _ => throw new Refusal("UNSUPPORTED_ACTION") }; Require(changedCode == 200 && changed["success"]?.GetValue() == true && Text(changed["license"]?["id"]) == licenseId && Text(changed["license"]?["status"]) == expected && (action != "renew" || changed["license"]?["expires_at"]?.GetValue() is not null), "MUTATION_RESULT_REQUIRES_REVIEW"); return (licenseId, null, changed["request_id"]?.GetValue()); } public async Task Work() { var lockPath = Path.Combine(directory, "worker.lock"); Require(new FileInfo(lockPath).LinkTarget is null, "INVALID_LOCK_FILE"); using var workerLock = new FileStream(lockPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); Require(flock(workerLock.SafeFileHandle.DangerousGetHandle().ToInt32(), 6) == 0, "WORKER_BUSY"); var processed = 0; try { while (processed < 25) { var claimed = Transaction(() => { var effect = One("SELECT e.* FROM effects e JOIN orders o ON o.order_key=e.order_key WHERE e.state='WAITING' AND o.held=0 AND e.sequence=o.last_sequence+1 ORDER BY e.created_at,e.effect_key LIMIT 1"); if (effect is null) return null; var order = One("SELECT * FROM orders WHERE order_key=@p0", effect["order_key"])!; Run("UPDATE effects SET state='IN_FLIGHT' WHERE effect_key=@p0", effect["effect_key"]); Run("UPDATE orders SET held=1 WHERE order_key=@p0", effect["order_key"]); return new[] { effect, order }; }); if (claimed is null) break; var effect = claimed[0]; try { var result = await Perform((JsonObject)JsonNode.Parse((string)effect["command_json"]!)!, claimed[1], (string)effect["effect_key"]!); Require(result.RequestId is null || result.RequestId.Length <= 190, "UNEXPECTED_REQUEST_ID"); Transaction(() => { if (result.Key is not null) { var nonce = RandomNumberGenerator.GetBytes(12); var plain = Utf8.GetBytes(result.Key); var cipher = new byte[plain.Length]; var tag = new byte[16]; using var aes = new AesGcm(outboxKey, 16); aes.Encrypt(nonce, plain, cipher, tag, Utf8.GetBytes((string)effect["order_key"]!)); Run("INSERT INTO outbox(order_key,license_id,cipher,created_at) VALUES(@p0,@p1,@p2,@p3)", effect["order_key"], result.Id, Convert.ToBase64String(nonce.Concat(cipher).Concat(tag).ToArray()), Now()); CryptographicOperations.ZeroMemory(plain); } Run("UPDATE effects SET state='DONE',request_id=@p0 WHERE effect_key=@p1", result.RequestId, effect["effect_key"]); Run("UPDATE orders SET license_id=@p0,last_sequence=@p1,held=0 WHERE order_key=@p2", result.Id, effect["sequence"], effect["order_key"]); return true; }); } catch { Run("UPDATE effects SET state='REVIEW',error_code='RECONCILE_BEFORE_RETRY' WHERE effect_key=@p0", effect["effect_key"]); } processed++; await Task.Delay(1100); } } finally { flock(workerLock.SafeFileHandle.DangerousGetHandle().ToInt32(), 8); } return new { processed, states = Inspect() }; } public Dictionary Inspect() { using var command = Command("SELECT state,COUNT(*) AS count FROM effects GROUP BY state"); using var rows = command.ExecuteReader(); var result = new Dictionary(); while (rows.Read()) result[rows.GetString(0)] = rows.GetInt64(1); return result; } public object Export(string order, string target) { Require(IsId(order), "INVALID_ORDER_ID"); var orderKey = Identity(Text(config["source"]), Text(config["seller_id"]), environment, order); var item = One("SELECT b.*,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key WHERE b.order_key=@p0", orderKey) ?? throw new Refusal("NO_SAVED_KEY"); Require(Path.IsPathFullyQualified(target), "PRIVATE_EXPORT_DIRECTORY_REQUIRED"); var parent = Path.GetDirectoryName(target)!; Require(Directory.Exists(parent) && ((int)File.GetUnixFileMode(parent) & 0x3F) == 0, "PRIVATE_EXPORT_DIRECTORY_REQUIRED"); var raw = Convert.FromBase64String((string)item["cipher"]!); Require(raw.Length >= 29, "INVALID_OUTBOX"); var plain = new byte[raw.Length - 28]; using var aes = new AesGcm(outboxKey, 16); aes.Decrypt(raw.AsSpan(0, 12), raw.AsSpan(12, raw.Length - 28), raw.AsSpan(raw.Length - 16), plain, Utf8.GetBytes(orderKey)); using var stream = new FileStream(target, FileMode.CreateNew, FileAccess.Write, FileShare.None); File.SetUnixFileMode(target, UnixFileMode.UserRead | UnixFileMode.UserWrite); var output = JsonSerializer.SerializeToUtf8Bytes(new { license_id = item["license_id"], license_key = Utf8.GetString(plain), email = item["email"] }, JsonOptions); stream.Write(output); stream.Flush(true); CryptographicOperations.ZeroMemory(plain); return new { exported = true, contains_secret = true }; } public async Task Serve() { var builder = WebApplication.CreateSlimBuilder(); builder.Logging.ClearProviders(); builder.WebHost.UseUrls("http://127.0.0.1:" + (Env("KDP_FULFILLMENT_PORT") is { Length: > 0 } port ? port : "8112")); builder.WebHost.ConfigureKestrel(options => { options.AddServerHeader = false; options.Limits.MaxRequestBodySize = Limit; options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(5); }); var server = builder.Build(); server.Run(async context => { var request = context.Request; int status = 202; object result = new { accepted = false }; try { if (request.Method != "POST" || request.Path != "/store-event" || request.QueryString.HasValue) status = 404; else if (request.Headers.ContainsKey("Transfer-Encoding") || request.ContentLength is null) status = 400; else if (request.ContentLength > Limit) status = 413; else if (request.Headers["X-Fulfillment-Signature"].Count != 1) status = 401; else if (request.ContentType?.Split(';')[0].Trim().ToLowerInvariant() != "application/json") status = 415; else { using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); using var buffer = new MemoryStream(); await request.Body.CopyToAsync(buffer, timeout.Token); Require(buffer.Length == request.ContentLength, "INVALID_BODY_LENGTH"); lock (receiverLock) result = Accept(buffer.ToArray(), request.Headers["X-Fulfillment-Signature"].ToString()); } } catch (Refusal error) { status = error.Message == "INVALID_SIGNATURE" ? 401 : 409; } catch (JsonException) { status = 400; } catch (DecoderFallbackException) { status = 400; } catch (BadHttpRequestException error) { status = error.StatusCode; } catch { status = 503; } context.Response.StatusCode = status; context.Response.ContentType = "application/json"; context.Response.Headers.CacheControl = "no-store"; context.Response.Headers.XContentTypeOptions = "nosniff"; await context.Response.WriteAsync(JsonSerializer.Serialize(result, JsonOptions)); }); Console.WriteLine("{\"listening\":true,\"loopback_only\":true}"); await server.RunAsync(); } public void Dispose() { http.Dispose(); db.Dispose(); } } ``` ## Expected result The actual local receiver and worker pass the shared lifecycle suite: durable receipt before work, authentic command acceptance, tamper/time/mode/mapping refusal, duplicate/conflicting effects, ordered gap handling, real creation and recipient [activation](/docs/v1/start/glossary#activation), renewal, suspension, restoration and terminal revocation. The suite also exports an encrypted saved key, refuses unsafe config/key drift, restarts the receiver, holds a simulated interrupted intent, and hides a genuinely successful remote create response behind a failing gateway. No second create is sent after that unknown result. These are exact-source local Sandbox tests on macOS. They are not an external store purchase, production deployment, native Windows test, email delivery test or promise of cross-system exactly-once behavior. The known API replay limitations remain unchanged. The read-before-write expected-state check is not an atomic remote compare-and-swap. ## Common mistakes - Sending a provider's native event directly here: this receiver accepts your verified adapter's private command format, not a Stripe/Paddle/PayPal callback. - Exposing the development server publicly without TLS, request bounds and rate protection. - Treating a 202 receipt or exported key file as proof of successful buyer email delivery. - Reusing a fresh state directory after an unknown create instead of reconciling the original order. ## Security notes - Keep config, state, encrypted-ledger backups and plaintext exports outside all web roots. - A backend that holds both the encryption key and database can decrypt its outbox; filesystem and process isolation still matter. - Keep one reviewed writer per order and preserve stable business-effect identity across delivery retries. - PHP SAPI can buffer bodies before script execution. Configure the ingress 256 KiB body limit and timeouts, not only application validation. - Recheck runtime support and dependency advisories before production deployment. The recorded advisory scan is not certification. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Safe configuration error | Missing/mismatched private mapping, key, permissions or runtime dependency | Verify private environment and required modules; never publish raw secrets to debug | | HTTP 401 | Wrong signature, timestamp, changed bytes or duplicate signature header | Sign exact bytes with the separate command secret and verify the clock | | HTTP 409 | Mapping/merchant/effect/sequence conflict | Reconcile trusted adapter state; do not invent a different effect ID | | No work processed | Already complete, sequence gap, held order or busy worker | Inspect saved state and authorized history before any write | | Export refused | Wrong order/key, unsafe target directory or file already exists | Verify recipient/order and use a new private owner-only target file | ## Next steps Review [the parent security checklist](/docs/v1/guides/any-store#security-notes), then test your real provider-specific adapter separately in its test environment. ## Related - [Generic store mapping and lifecycle](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Manual delivery and customer lookup](/docs/v1/guides/manual-issuance) # Run the store integration service Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service --- title: "Run the store integration service" description: "Run the store integration service for the complete private reference application." slug: "/docs/v1/guides/store-service" section: "Webhooks and integrations" order: 790 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Run one dedicated PHP reference backend for one provider/account/environment. It receives native provider notifications and fulfills them through the existing KDPika API. It does not change KDPika's own checkout, billing processor, database schema or SDK. ## What you will learn - Deploy safely on PHP shared hosting without Docker, Node, root or a daemon. - Configure private credentials, approved offer mapping and durable state. - Run bounded work/mail cron jobs and review uncertain results. ## Prerequisites PHP 8.2+ with curl, pdo_sqlite, mbstring and openssl; a private persistent POSIX filesystem; verified HTTPS; and hosting cron. Confirm the same extensions in web PHP and CLI PHP. SQLite files belong to this [developer](/docs/v1/start/glossary#developer) reference, not KDPika's MySQL schema. Read the [generic trust boundary](/docs/v1/guides/any-store) and select the provider guide before configuring notifications. Download the [complete source archive](/docs/downloads/store-integrations.zip) and [source manifest](/docs/store-examples.json). All source modules and deliberately synthetic fixtures are included; no credentials, state, generated binaries or buyer keys are packaged. ## Choose your provider :::cards - [Stripe](/docs/v1/guides/stripe) - [Paddle Billing](/docs/v1/guides/paddle) - [Lemon Squeezy](/docs/v1/guides/lemon-squeezy) - [Gumroad](/docs/v1/guides/gumroad) - [WooCommerce](/docs/v1/guides/woocommerce) - [Shopify](/docs/v1/guides/shopify) - [PayPal](/docs/v1/guides/paypal) - [FastSpring](/docs/v1/guides/fastspring) ::: ## Install the files Use a dedicated subdomain/backend directory, not your store's existing document root. Replace ACCOUNT and /home/ACCOUNT/license-events with your actual hosting account/path. A suitable layout is: ```text /home/ACCOUNT/license-events/ public/ dedicated HTTPS document root index.php copied from stores/public/index.php .htaccess copied from stores/public/.htaccess private/kdpika-store/ entire archive extracted here stores/ fulfillment/ environment.php copy stores/environment.example.php config.json copy stores/config.example.json fulfillment.json copy fulfillment/config.example.json state/ created privately by the worker ``` Keep private directories 0700 and configuration/files 0600. The public bootstrap resolves its sibling private/kdpika-store and loads no credentials from a URL. Only copy the two public bootstrap files into this dedicated document root. Do not upload the entire archive or its private state into public_html. Never overwrite an existing store's index.php or .htaccess. ## Map the purchase to a product plan 1. In private config.json select the provider and exact environment [sandbox](/docs/v1/start/glossary#sandbox). Set seller_id and provider-specific fields using its guide. Remove no validation by changing source. 2. Map the verified provider price/variant/[product](/docs/v1/start/glossary#product) reference to an internal offer in offer_map. These mapping keys need not have the same format; they are deliberately two independent boundaries. 3. In private fulfillment.json set source to the exact provider string, seller_id to the same stable merchant identity, environment to sandbox, base_url to https://licensing.kdpika.com and product_id to your product. 4. Under offers map that internal offer to your real KDPika plan_id and explicitly chosen renewal_days. Use a fixed-duration plan when selling limited periods; a perpetual initial plan will not become a timed subscription merely because renewal_days is present. 5. For this PHP reference add create_idempotency with value effect_id to fulfillment.json. This retains the literal Stripe Session/Event ID for its initial create. It is a PHP reference option, not a public KDPika API setting or a [feature](/docs/v1/start/glossary#feature) of the other three generic runtimes. ```json { "provider": "stripe", "environment": "sandbox", "seller_id": "YOUR_STORE_ACCOUNT", "shop_domain": "YOUR_SHOP.myshopify.com", "webhook_id": "YOUR_PAYPAL_WEBHOOK_ID", "api_version": "2025-07-30.basil", "offer_map": { "YOUR_PROVIDER_PRICE_OR_VARIANT_ID": "YOUR_VERIFIED_PRICE_ID" } } ``` The shared [fulfillment configuration](/docs/v1/guides/any-store#configure-an-approved-offer-mapping) defines product, plan and renewal behavior. Initial creation uses the stored plan duration; renewal adds integer days, not an exact provider calendar end. There is no automatic in-place plan migration, proration or multi-line allocation. ## Load private credentials Copy this example to environment.php at the private archive root. Replace every YOUR_ value using private hosting configuration. Generate a separate unpredictable command secret and a base64 encoding of 32 random bytes for the encrypted key outbox. Keep an encrypted backup of the outbox key separately from state; losing it prevents saved-key [delivery](/docs/v1/start/glossary#delivery). ```php $private . '/config.json', 'KDP_STORE_STATE' => $private . '/state/store', 'KDP_STORE_WEBHOOK_SECRET' => 'YOUR_PROVIDER_ENDPOINT_SIGNING_SECRET', 'KDP_STORE_API_KEY' => 'YOUR_PROVIDER_API_KEY', 'KDP_STORE_API_SECRET' => 'YOUR_PROVIDER_API_SECRET_IF_BASIC_AUTH', 'KDP_STORE_CLIENT_ID' => 'YOUR_PAYPAL_CLIENT_ID_IF_PAYPAL', 'KDP_STORE_CLIENT_SECRET' => 'YOUR_PAYPAL_CLIENT_SECRET_IF_PAYPAL', 'KDP_FULFILLMENT_CONFIG' => $private . '/fulfillment.json', 'KDP_FULFILLMENT_STATE' => $private . '/state/fulfillment', 'KDP_FULFILLMENT_SECRET' => 'YOUR_SEPARATE_UNPREDICTABLE_COMMAND_SECRET', 'KDP_FULFILLMENT_OUTBOX_KEY' => 'YOUR_BASE64_ENCODED_32_RANDOM_BYTES', 'KDP_API_KEY' => 'YOUR_PRODUCT_SCOPED_SANDBOX_API_KEY', 'KDP_SMTP_HOST' => 'YOUR_SMTP_HOST', 'KDP_SMTP_PORT' => '587', 'KDP_SMTP_USERNAME' => 'YOUR_SMTP_USERNAME', 'KDP_SMTP_PASSWORD' => 'YOUR_SMTP_PASSWORD', 'KDP_SMTP_FROM' => 'support@example.invalid', 'KDP_SOFTWARE_NAME' => 'Example application', ]; ``` Provider API keys are not the product-scoped KDP_API_KEY. Keep the provider destination secret separate from KDP_FULFILLMENT_SECRET and outbound LMP [webhook](/docs/v1/start/glossary#webhook) secrets. For PayPal automatic OAuth set KDP_STORE_API_KEY to an empty string and fill its private client ID/secret. Remove unused provider-specific secrets from your private copy rather than replacing them with other accounts' credentials. Configuration is fingerprint-bound to existing ledgers. Review migrations before changing account, source, mapping or outbox key. Rotating an API credential is not permission to delete state. Production must not contain KDP_STORE_TEST_API, KDP_STORE_ALLOW_LOOPBACK, KDP_ALLOW_LOOPBACK or KDP_SMTP_ALLOW_LOOPBACK test overrides. ## Run the private cron jobs Check your hosting panel's PHP CLI path and version. A typical cPanel command is below; /usr/local/bin/php is an example, not a guarantee for every host. Some hosts use /opt/cpanel/ea-php82/root/usr/bin/php or another versioned binary. Select the same supported version/extensions used by web PHP. ```bash /usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php inspect ``` Expected: safe aggregate states JSON, never a full key. Configure these two commands every minute in Cron Jobs: ```bash /usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php work /usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php mail ``` They are separate cron entries with schedule * * * * *. Do not create a public HTTP cron URL. work processes at most 20 inbox events per run; its shared licensing worker processes at most 25 effects and paces writes. mail processes at most 25 new saved keys. Overlapping workers use nonblocking private locks; inspect safe failures rather than disabling locking. Configure private cron failure notifications with your host. ## Receipt, work and recovery POST /provider-event is the only public route. A valid durable receipt returns HTTP 202 and accepted:true with duplicate:false, or duplicate:true for an unchanged delivery. Invalid proof, conflict, excess body and overload do not return successful receipt. The whole-service cap is 60 incoming requests/minute and raw body size is at most 262144 bytes. This basic cap is not a substitute for hosting firewall/edge controls. Provider reads use fixed HTTPS origins, verified TLS, bounded response sizes and no redirects. The optional [Node ingress](/docs/v1/guides/store-service/node) and [Python ingress](/docs/v1/guides/store-service/python) preserve raw bytes and only acknowledge after the PHP service commits. The direct PHP path requires neither relay. The worker stores native decisions and stable commands before dispatch. A paid receipt is applied once; a missing initial mapping, changed recipient/offer, partial refund or uncertain remote result is held for review. A full refund before known issuance leaves an adverse-reference record, so a later matching paid notification is not blindly granted. This is conservative reconciliation, not universal causal ordering or cross-system exactly-once delivery. If inspect reports REVIEW or IN_FLIGHT, check the private provider ledger, current provider objects, licensing effects and dashboard before another write. No automatic reset/reissue command is supplied. Preserve backups and stable business identities. Do not manually delete a failed row, invent a new order or rerun a possibly successful additive renewal. ## Deliver the saved key Use [the SMTP outbox guide](/docs/v1/guides/store-service/email). Issuance and delivery have separate durable acknowledgements. A saved key can be exported through the [PHP fulfillment reference](/docs/v1/guides/any-store/php) for an authenticated recipient, without creating another [license](/docs/v1/start/glossary#license). ## Source and verification :::cards - [Durable service source](/docs/v1/guides/store-service/worker) - [Authentication and provider HTTP source](/docs/v1/guides/store-service/security) - [Common normalizer](/docs/v1/guides/store-service/normalizer) - [Public/cron bootstrap source](/docs/v1/guides/store-service/bootstrap) ::: The exact sources were run against synthetic provider contracts and an actual isolated licensing API. Local contracts are not real merchant account purchases or production host deployment. Run the chosen provider's Test procedure, real HTTPS delivery and SMTP inbox test on your backend before [Live](/docs/v1/start/glossary#live) use. Native Windows ACLs/locks are not claimed; use a POSIX backend or implement and verify an appropriate private filesystem/locking adapter. ## Retention and support Raw inboxes and saved decisions may contain buyer metadata. Keep them private, restrict operator access, document your own retention/deletion process and encrypt backups. Do not automatically delete the durable business-effect identities needed to prevent duplicate issuance; separate minimized identifiers from raw PII when implementing a reviewed retention migration. No automatic PII-retention policy is invented by this example. Never send SQLite files, raw events, credentials or full keys in public support reports. ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: durable worker source Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/worker --- title: "Store service: durable worker source" description: "Store service: durable worker source for the complete private reference application." slug: "/docs/v1/guides/store-service/worker" section: "Webhooks and integrations" order: 791 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- This is the complete PHP service used by every provider guide. It requires the other exact archive files and private configuration from [installation](/docs/v1/guides/store-service); copying this file alone is not a deployment. ## What you will learn Read and run the exact Store service: durable worker source with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Complete source ```php config = json_decode((string) file_get_contents($file), true, 64, JSON_THROW_ON_ERROR); storeRequire(in_array($this->config['provider'] ?? '', ['stripe', 'paddle', 'lemon-squeezy', 'gumroad', 'woocommerce', 'shopify', 'paypal', 'fastspring'], true), 'INVALID_PROVIDER'); storeRequire(in_array($this->config['environment'] ?? '', ['sandbox', 'live'], true), 'INVALID_ENVIRONMENT'); storeRequire($this->config['environment'] !== 'live' || getenv('KDP_CONFIRM_LIVE') === '1', 'LIVE_CONFIRMATION_REQUIRED'); storeRequire(is_string($this->config['seller_id'] ?? null) && $this->config['seller_id'] !== '' && is_array($this->config['offer_map'] ?? null) && $this->config['offer_map'] !== [], 'PRIVATE_MAPPING_REQUIRED'); storeRequire(($this->config['provider'] !== 'stripe') || ($this->config['api_version'] ?? '') === '2025-07-30.basil', 'PINNED_STRIPE_VERSION_REQUIRED'); $this->fulfillmentConfig = json_decode((string) file_get_contents((string) getenv('KDP_FULFILLMENT_CONFIG')), true, 64, JSON_THROW_ON_ERROR); storeRequire($this->fulfillmentConfig['environment'] === $this->config['environment'] && $this->fulfillmentConfig['source'] === $this->config['provider'], 'FULFILLMENT_MAPPING_MISMATCH'); foreach ($this->config['offer_map'] as $offer) { storeRequire(isset($this->fulfillmentConfig['offers'][$offer]), 'UNMAPPED_FULFILLMENT_OFFER'); } $this->directory = (string) getenv('KDP_STORE_STATE'); storeRequire(str_starts_with($this->directory, '/') && !is_link($this->directory), 'PRIVATE_STORE_STATE_REQUIRED'); if (!is_dir($this->directory)) { mkdir($this->directory, 0700, true); } storeRequire((fileperms($this->directory) & 0077) === 0, 'PRIVATE_STATE_PERMISSIONS_REQUIRED'); $database = $this->directory . '/store.sqlite'; storeRequire(!is_link($database), 'INVALID_STATE_FILE'); $this->db = new PDO('sqlite:' . $database, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); chmod($database, 0600); $this->db->exec("PRAGMA busy_timeout=3000; CREATE TABLE IF NOT EXISTS inbox(id TEXT PRIMARY KEY,body_hash TEXT NOT NULL,native TEXT NOT NULL,headers TEXT NOT NULL,state TEXT NOT NULL,decisions TEXT,received_at INTEGER NOT NULL,error_code TEXT); CREATE TABLE IF NOT EXISTS links(reference TEXT PRIMARY KEY,order_id TEXT NOT NULL); CREATE TABLE IF NOT EXISTS unresolved_denials(reference TEXT PRIMARY KEY,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS payments(order_id TEXT NOT NULL,receipt TEXT NOT NULL,PRIMARY KEY(order_id,receipt)); CREATE TABLE IF NOT EXISTS commands(id TEXT PRIMARY KEY,inbox_id TEXT NOT NULL,position INTEGER NOT NULL,body TEXT NOT NULL,order_id TEXT NOT NULL,receipt TEXT NOT NULL,state TEXT NOT NULL); CREATE TABLE IF NOT EXISTS binding(id INTEGER PRIMARY KEY CHECK(id=1),config_hash TEXT NOT NULL); CREATE TABLE IF NOT EXISTS ingress(window INTEGER PRIMARY KEY,requests INTEGER NOT NULL);"); $hash = hash('sha256', commandJson($this->config)); $this->run('INSERT OR IGNORE INTO binding(id,config_hash) VALUES(1,?)', [$hash]); storeRequire($this->one('SELECT config_hash FROM binding WHERE id=1')['config_hash'] === $hash, 'CONFIGURATION_CHANGED_RECONCILE_FIRST'); $this->fulfillment = new FulfillmentReference(); $this->fulfillmentDb = new PDO('sqlite:' . rtrim((string) getenv('KDP_FULFILLMENT_STATE'), '/') . '/fulfillment.sqlite', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); $this->fulfillmentDb->exec('PRAGMA busy_timeout=3000'); $this->remote = storeApi($this->config); } private function run(string $sql, array $values = []): PDOStatement { $statement = $this->db->prepare($sql); $statement->execute($values); return $statement; } private function one(string $sql, array $values = []): array|false { return $this->run($sql, $values)->fetch(); } private function transaction(callable $operation): mixed { $this->db->exec('BEGIN IMMEDIATE'); try { $result = $operation(); $this->db->exec('COMMIT'); return $result; } catch (Throwable $error) { $this->db->exec('ROLLBACK'); throw $error; } } public function accept(string $body, array $headers): array { // Whole-service cap also bounds remote verification calls for unsigned Ping/PayPal. $this->transaction(function (): void { $window = (int) floor(time() / 60); $this->run('DELETE FROM ingress WHERE windowrun('INSERT INTO ingress(window,requests) VALUES(?,1) ON CONFLICT(window) DO UPDATE SET requests=requests+1', [$window]); storeRequire((int) $this->one('SELECT requests FROM ingress WHERE window=?', [$window])['requests'] <= 60, 'INGRESS_RATE_LIMITED'); }); $authenticated = storeAuthenticate($this->config['provider'], $body, $headers, $this->config, $this->remote); $native = $authenticated['native']; if ($this->config['provider'] === 'fastspring') { $events = $native['events'] ?? []; storeRequire(is_array($events) && array_is_list($events) && count($events) >= 1 && count($events) <= 50, 'INVALID_EVENT_BATCH'); return $this->transaction(function () use ($events, $headers): array { $allDuplicate = true; foreach ($events as $event) { storeRequire(is_array($event) && is_string($event['id'] ?? null) && strlen($event['id']) >= 1 && strlen($event['id']) <= 190, 'EVENT_REFERENCE_REQUIRED'); $id = commandIdentity('fastspring', $this->config['seller_id'], $this->config['environment'], $event['id']); $hash = hash('sha256', commandJson($event)); $previous = $this->one('SELECT body_hash FROM inbox WHERE id=?', [$id]); if ($previous) { storeRequire($previous['body_hash'] === $hash, 'EVENT_CONFLICT'); continue; } $allDuplicate = false; $this->run("INSERT INTO inbox(id,body_hash,native,headers,state,received_at) VALUES(?,?,?,?,'WAITING',?)", [$id, $hash, json_encode(['native' => ['events' => [$event]]], JSON_THROW_ON_ERROR), json_encode($headers, JSON_THROW_ON_ERROR), time()]); } return ['accepted' => true, 'duplicate' => $allDuplicate]; }); } $identifier = match ($this->config['provider']) { 'stripe', 'paypal' => $native['id'] ?? null, 'paddle' => $native['event_id'] ?? null, 'shopify' => storeHeader($headers, 'x-shopify-webhook-id'), 'fastspring' => $native['events'][0]['id'] ?? null, default => hash('sha256', $body) }; storeRequire(is_string($identifier) && $identifier !== '' && strlen($identifier) <= 190, 'EVENT_REFERENCE_REQUIRED'); $id = commandIdentity($this->config['provider'], $this->config['seller_id'], $this->config['environment'], $identifier); $hash = hash('sha256', $body); return $this->transaction(function () use ($id, $hash, $authenticated, $headers): array { $previous = $this->one('SELECT body_hash FROM inbox WHERE id=?', [$id]); if ($previous) { storeRequire($previous['body_hash'] === $hash, 'EVENT_CONFLICT'); return ['accepted' => true, 'duplicate' => true]; } $this->run("INSERT INTO inbox(id,body_hash,native,headers,state,received_at) VALUES(?,?,?,?,'WAITING',?)", [$id, $hash, json_encode($authenticated, JSON_THROW_ON_ERROR), json_encode($headers, JSON_THROW_ON_ERROR), time()]); return ['accepted' => true, 'duplicate' => false]; }); } private function license(string $id): array { $origin = rtrim($this->fulfillmentConfig['base_url'], '/'); $response = storeRequest($origin, 'GET', '/api/v1/licenses/' . rawurlencode($id), ['Accept: application/json', 'Authorization: Bearer ' . (string) getenv('KDP_API_KEY')]); storeRequire(($response['success'] ?? false) && ($response['license']['id'] ?? '') === $id && ($response['license']['environment'] ?? '') === $this->config['environment'], 'LICENSE_NOT_CONFIRMED'); return $response['license']; } private function order(string $id): array|false { $hash = commandIdentity($this->fulfillmentConfig['source'], $this->fulfillmentConfig['seller_id'], $this->config['environment'], $id); $statement = $this->fulfillmentDb->prepare('SELECT * FROM orders WHERE order_key=?'); $statement->execute([$hash]); return $statement->fetch(); } private function prepare(array $event, array $decision, int $position): ?array { $existing = $this->one('SELECT * FROM commands WHERE inbox_id=? AND position=?', [$event['id'], $position]); if ($existing) { return $existing; } $rawOrder = $decision['order']; $orderId = 'order_' . hash('sha256', $rawOrder); $order = $this->order($orderId); $current = null; storeRequire(!$order || (int) $order['held'] === 0, 'ORDER_HELD_RECONCILE_FIRST'); if ($order && $order['license_id']) { $current = $this->license($order['license_id']); } $state = $decision['state']; if ($state === 'paid') { if ($this->one('SELECT receipt FROM payments WHERE order_id=? AND receipt=?', [$rawOrder, $decision['receipt']])) { return null; } if (!$order) { foreach ($decision['links'] as $reference) { storeRequire(!$this->one('SELECT state FROM unresolved_denials WHERE reference=?', [$reference]), 'EARLIER_ADVERSE_EVENT_REQUIRES_REVIEW'); } storeRequire(($decision['initial'] ?? false) === true, 'INITIAL_PURCHASE_NOT_RECORDED'); $action = 'issue'; } else { storeRequire($current !== null && $decision['recurring'] === true && $order['offer_id'] === $decision['offer'] && $order['email'] === $decision['customer']['email'], 'PLAN_OR_CUSTOMER_CHANGE_REQUIRES_REVIEW'); $action = ($current['status'] ?? '') === 'suspended' ? 'restore' : 'renew'; } } else { storeRequire($current !== null, 'PURCHASE_MAPPING_REQUIRES_REVIEW'); $action = match ($state) { 'failed', 'disputed' => 'suspend', 'refunded' => 'revoke', 'ended' => 'suspend', 'cancelled' => 'record_cancellation', 'recovered' => 'restore', default => throw new StoreRefusal('UNSUPPORTED_DECISION') }; if (($action === 'suspend' && $current['status'] === 'suspended') || ($action === 'restore' && $current['status'] === 'active') || ($action === 'revoke' && $current['status'] === 'revoked')) { return null; } } $effect = ($action === 'issue' && $this->config['provider'] === 'stripe') ? $decision['initial_key'] : null; if (!is_string($effect) || strlen($effect) < 8 || !commandId($effect)) { $effect = 'effect_' . hash('sha256', $event['id'] . ':' . $position . ':' . $action); } $command = ['version' => 1, 'source' => $this->fulfillmentConfig['source'], 'seller_id' => $this->fulfillmentConfig['seller_id'], 'environment' => $this->config['environment'], 'event_id' => 'delivery_' . $event['id'] . '_' . $position, 'effect_id' => $effect, 'order_id' => $orderId, 'sequence' => (int) ($order['last_sequence'] ?? 0) + 1, 'action' => $action, 'offer_id' => $order ? $order['offer_id'] : $decision['offer'], 'customer' => $order ? ['email' => $order['email'], 'name' => 'Verified store buyer'] : $decision['customer'], 'expected_status' => $current['status'] ?? null, 'expected_expires_at' => $current['expires_at'] ?? null]; $body = commandJson($command); $id = hash('sha256', $event['id'] . ':' . $position); $this->run("INSERT INTO commands(id,inbox_id,position,body,order_id,receipt,state) VALUES(?,?,?,?,?,?,'WAITING')", [$id, $event['id'], $position, $body, $rawOrder, $decision['receipt']]); return $this->one('SELECT * FROM commands WHERE id=?', [$id]); } private function execute(array $job): string { $stamp = (string) time(); $signature = 't=' . $stamp . ',v1=' . hash_hmac('sha256', $stamp . '.' . $job['body'], (string) getenv('KDP_FULFILLMENT_SECRET')); $this->fulfillment->accept($job['body'], $signature); $this->fulfillment->work(); $command = json_decode($job['body'], true, 64, JSON_THROW_ON_ERROR); $hash = commandIdentity($command['source'], $command['seller_id'], $command['environment'], $command['effect_id']); $query = $this->fulfillmentDb->prepare('SELECT state FROM effects WHERE effect_key=?'); $query->execute([$hash]); storeRequire($query->fetchColumn() === 'DONE', 'FULFILLMENT_RESULT_REQUIRES_REVIEW'); $this->run("UPDATE commands SET state='DONE' WHERE id=?", [$job['id']]); return $command['action']; } public function work(): array { $path = $this->directory . '/worker.lock'; storeRequire(!is_link($path), 'INVALID_LOCK'); $lock = fopen($path, 'c'); storeRequire($lock !== false && flock($lock, LOCK_EX | LOCK_NB), 'STORE_WORKER_BUSY'); $processed = 0; try { $events = $this->run("SELECT * FROM inbox WHERE state='WAITING' ORDER BY received_at,id LIMIT 20")->fetchAll(); foreach ($events as $event) { try { $normalizer = new StoreNormalizer($this->config, $this->remote, fn(string $reference): ?string => ($this->one('SELECT order_id FROM links WHERE reference=?', [$reference]) ?: [])['order_id'] ?? null); $decisions = $event['decisions'] === null ? $normalizer->normalize(json_decode($event['native'], true, 64, JSON_THROW_ON_ERROR), json_decode($event['headers'], true, 64, JSON_THROW_ON_ERROR)) : json_decode($event['decisions'], true, 64, JSON_THROW_ON_ERROR); $this->run('UPDATE inbox SET decisions=? WHERE id=?', [json_encode($decisions, JSON_THROW_ON_ERROR), $event['id']]); foreach ($decisions as $position => $decision) { if (!empty($decision['unmapped_reference'])) { $this->run('INSERT OR IGNORE INTO unresolved_denials(reference,state) VALUES(?,?)', [$decision['unmapped_reference'], $decision['state']]); throw new StoreRefusal('ADVERSE_EVENT_PRECEDES_PURCHASE_RECONCILE_FIRST'); } $job = $this->prepare($event, $decision, $position * 2); $action = $job ? $this->execute($job) : null; // A recovered paid period needs both restore and one additive renewal. if ($decision['state'] === 'paid' && $action === 'restore') { $second = $this->prepare($event, $decision, $position * 2 + 1); storeRequire($second !== null, 'RENEWAL_COMMAND_REQUIRED'); $this->execute($second); } $this->transaction(function () use ($decision): void { if ($decision['state'] === 'paid') { $this->run('INSERT OR IGNORE INTO payments(order_id,receipt) VALUES(?,?)', [$decision['order'], $decision['receipt']]); } foreach ($decision['links'] as $reference) { if ($reference === '') { continue; } $previous = $this->one('SELECT order_id FROM links WHERE reference=?', [$reference]); storeRequire(!$previous || $previous['order_id'] === $decision['order'], 'PURCHASE_REFERENCE_CONFLICT'); $this->run('INSERT OR IGNORE INTO links(reference,order_id) VALUES(?,?)', [$reference, $decision['order']]); } }); } $this->run("UPDATE inbox SET state='DONE' WHERE id=?", [$event['id']]); } catch (Throwable $error) { $code = $error instanceof StoreRefusal && preg_match('/\A[A-Z_]{1,80}\z/D', $error->getMessage()) ? $error->getMessage() : 'RECONCILE_PROVIDER_AND_FULFILLMENT'; $this->run("UPDATE inbox SET state='REVIEW',error_code=? WHERE id=?", [$code, $event['id']]); } $processed++; } } finally { flock($lock, LOCK_UN); fclose($lock); } return ['processed' => $processed, 'states' => $this->inspect()]; } public function inspect(): object { $states = []; foreach ($this->run('SELECT state,COUNT(*) AS count FROM inbox GROUP BY state')->fetchAll() as $row) { $states[$row['state']] = (int) $row['count']; } return (object) $states; } } if (defined('KDP_STORE_LIBRARY_ONLY') && KDP_STORE_LIBRARY_ONLY === true) { return; } try { $service = new StoreService(); if (PHP_SAPI === 'cli') { if (($argv[1] ?? '') === 'work' && $argc === 2) { $result = $service->work(); } elseif (($argv[1] ?? '') === 'inspect' && $argc === 2) { $result = ['states' => $service->inspect()]; } else { throw new StoreRefusal('USE_WORK_OR_INSPECT'); } echo json_encode($result, JSON_THROW_ON_ERROR) . PHP_EOL; exit; } if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || ($_SERVER['REQUEST_URI'] ?? '') !== '/provider-event') { referenceReply(404, ['accepted' => false]); } if (!ctype_digit((string) ($_SERVER['CONTENT_LENGTH'] ?? '')) || isset($_SERVER['HTTP_TRANSFER_ENCODING'])) { referenceReply(400, ['accepted' => false]); } if ((int) $_SERVER['CONTENT_LENGTH'] > 262144) { referenceReply(413, ['accepted' => false]); } $headers = []; foreach (getallheaders() as $name => $value) { $headers[strtolower($name)] = $value; } $allowed = ['stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time']; $headers = array_intersect_key($headers, array_flip($allowed)); $raw = file_get_contents('php://input', false, null, 0, 262145); storeRequire(is_string($raw) && strlen($raw) === (int) $_SERVER['CONTENT_LENGTH'], 'INVALID_BODY'); referenceReply(202, $service->accept($raw, $headers)); } catch (Throwable $error) { if (PHP_SAPI === 'cli') { echo '{"completed":false,"error":"REVIEW_PRIVATE_STORE_STATE"}' . PHP_EOL; exit(1); } $status = $error instanceof StoreRefusal ? match ($error->getMessage()) { 'INGRESS_RATE_LIMITED' => 429, 'EVENT_CONFLICT' => 409, 'INVALID_BODY' => 400, default => 401 } : 503; referenceReply($status, ['accepted' => false]); } ``` ## Expected result An authenticated request is durably saved before a 202 receipt. work and inspect print only aggregate states; unknown effects are held instead of blindly repeated. Private SQLite state and the fulfillment ledger must survive restarts. [Provider guides](/docs/v1/guides/store-service#choose-your-provider) define accepted payment evidence and unsupported cases. Native events are not public licensing commands. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: native proof and HTTP client Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/security --- title: "Store service: native proof and HTTP client" description: "Store service: native proof and HTTP client for the complete private reference application." slug: "/docs/v1/guides/store-service/security" section: "Webhooks and integrations" order: 792 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- These complete PHP helpers belong to the [store application](/docs/v1/guides/store-service). Native provider proof is deliberately separate from outbound X-LMP-Signature and the private fulfillment-command signature. ## What you will learn Read and run the exact Store service: native proof and HTTP client with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Native authentication ```php = 1 && count($parts[$hashKey]) <= 8, 'INVALID_SIGNATURE'); $stamp = $parts[$timeKey][0]; storeRequire(preg_match('/\A[1-9][0-9]{0,10}\z/D', $stamp) === 1 && abs($clock - (int) $stamp) <= $tolerance, 'INVALID_SIGNATURE'); $expected = hash_hmac('sha256', $stamp . ($timeKey === 'ts' ? ':' : '.') . $raw, $secret); $valid = false; foreach ($parts[$hashKey] as $candidate) { storeRequire(preg_match('/\A[0-9a-f]{64}\z/D', $candidate) === 1, 'INVALID_SIGNATURE'); $valid = hash_equals($expected, $candidate) || $valid; } storeRequire($valid, 'INVALID_SIGNATURE'); } function storeAuthenticate(string $provider, string $raw, array $headers, array $config, callable $remote, ?int $clock = null): array { storeRequire(strlen($raw) <= 262144, 'BODY_TOO_LARGE'); $clock ??= time(); $secret = (string) getenv('KDP_STORE_WEBHOOK_SECRET'); if (in_array($provider, ['stripe', 'paddle', 'lemon-squeezy', 'woocommerce', 'shopify', 'fastspring'], true)) { storeRequire(strlen($secret) >= 16, 'WEBHOOK_SECRET_REQUIRED'); } if ($provider === 'stripe') { storeTimestampHmac($raw, storeHeader($headers, 'stripe-signature'), $secret, ',', 't', 'v1', 300, $clock); } elseif ($provider === 'paddle') { storeTimestampHmac($raw, storeHeader($headers, 'paddle-signature'), $secret, ';', 'ts', 'h1', 5, $clock); } elseif ($provider === 'lemon-squeezy') { $signature = storeHeader($headers, 'x-signature'); storeRequire(preg_match('/\A[0-9a-f]{64}\z/D', $signature) === 1 && hash_equals(hash_hmac('sha256', $raw, $secret), $signature), 'INVALID_SIGNATURE'); } elseif (in_array($provider, ['woocommerce', 'shopify', 'fastspring'], true)) { $name = ['woocommerce' => 'x-wc-webhook-signature', 'shopify' => 'x-shopify-hmac-sha256', 'fastspring' => 'x-fs-signature'][$provider]; $signature = storeHeader($headers, $name); storeRequire(hash_equals(base64_encode(hash_hmac('sha256', $raw, $secret, true)), $signature), 'INVALID_SIGNATURE'); if ($provider === 'shopify') { storeRequire(storeHeader($headers, 'x-shopify-shop-domain') === ($config['shop_domain'] ?? null), 'WRONG_SHOP'); storeRequire(in_array(storeHeader($headers, 'x-shopify-topic'), ['orders/paid', 'refunds/create', 'orders/cancelled'], true), 'UNSUPPORTED_TOPIC'); } } elseif ($provider === 'gumroad') { // Gumroad Ping has no documented native HMAC. A sale ID is only a lookup hint. parse_str($raw, $hint); $id = $hint['sale_id'] ?? ''; storeRequire(is_string($id) && strlen($id) >= 4 && strlen($id) <= 190, 'SALE_REFERENCE_REQUIRED'); $verified = $remote('GET', '/v2/sales/' . rawurlencode($id), null); storeRequire(($verified['success'] ?? null) === true && ($verified['sale']['id'] ?? null) === $id, 'SALE_NOT_VERIFIED'); storeRequire(($verified['sale']['seller_id'] ?? null) === $config['seller_id'], 'WRONG_SELLER'); return ['native' => $hint, 'verified_sale' => $verified['sale']]; } elseif ($provider === 'paypal') { $event = json_decode($raw, true, 64, JSON_THROW_ON_ERROR); $request = ['webhook_id' => $config['webhook_id'], 'webhook_event' => $event]; foreach (['auth_algo', 'cert_url', 'transmission_id', 'transmission_sig', 'transmission_time'] as $field) { $request[$field] = storeHeader($headers, 'paypal-' . str_replace('_', '-', $field)); storeRequire($request[$field] !== '', 'INVALID_PAYPAL_HEADERS'); } // No fetching of an untrusted certificate URL by this application. $result = $remote('POST', '/v1/notifications/verify-webhook-signature', $request); storeRequire(($result['verification_status'] ?? null) === 'SUCCESS', 'INVALID_SIGNATURE'); } else { throw new StoreRefusal('UNSUPPORTED_PROVIDER'); } $event = json_decode($raw, true, 64, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING); storeRequire(is_array($event) && !array_is_list($event), 'INVALID_EVENT'); return ['native' => $event]; } ``` ## Fixed-origin provider client ```php $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_TIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_PROTOCOLS => str_starts_with($origin, 'https://') ? CURLPROTO_HTTPS : CURLPROTO_HTTP, CURLOPT_WRITEFUNCTION => static function ($unused, string $chunk) use (&$raw): int { if (strlen($raw) + strlen($chunk) > 1048576) { return 0; } $raw .= $chunk; return strlen($chunk); }]; if ($basic !== null) { $options[CURLOPT_USERPWD] = $basic; $options[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; } if ($body !== null) { $options[CURLOPT_POSTFIELDS] = is_string($body) ? $body : json_encode($body, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES); } curl_setopt_array($handle, $options); $sent = curl_exec($handle); $status = (int) curl_getinfo($handle, CURLINFO_RESPONSE_CODE); storeRequire($sent !== false && $status >= 200 && $status < 300, 'PROVIDER_API_UNCONFIRMED'); $value = json_decode($raw, true, 64, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING); storeRequire(is_array($value) && !array_is_list($value), 'INVALID_PROVIDER_RESPONSE'); return $value; } function storeApi(array $config): Closure { $provider = $config['provider']; $sandbox = $config['environment'] === 'sandbox'; $origins = ['stripe' => 'https://api.stripe.com', 'paddle' => $sandbox ? 'https://sandbox-api.paddle.com' : 'https://api.paddle.com', 'lemon-squeezy' => 'https://api.lemonsqueezy.com', 'gumroad' => 'https://api.gumroad.com', 'paypal' => $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com', 'fastspring' => 'https://api.fastspring.com']; if ($provider === 'shopify') { storeRequire(preg_match('/\A[a-z0-9][a-z0-9-]*\.myshopify\.com\z/D', $config['shop_domain']) === 1, 'INVALID_SHOP'); $origin = 'https://' . $config['shop_domain']; } elseif ($provider === 'woocommerce') { $origin = $config['store_url'] ?? ''; $url = parse_url($origin); storeRequire($url && ($url['scheme'] ?? '') === 'https' && !empty($url['host']) && !isset($url['user']) && !isset($url['pass']) && empty($url['path']) && !isset($url['query']) && !isset($url['fragment']), 'INVALID_STORE_ORIGIN'); } else { $origin = $origins[$provider] ?? throw new StoreRefusal('UNSUPPORTED_PROVIDER'); } $test = (string) getenv('KDP_STORE_TEST_API'); if ($test !== '') { $url = parse_url($test); storeRequire($sandbox && getenv('KDP_STORE_ALLOW_LOOPBACK') === '1' && ($url['scheme'] ?? '') === 'http' && ($url['host'] ?? '') === '127.0.0.1' && empty($url['path']) && !isset($url['user']), 'INVALID_TEST_API'); $origin = $test; } $key = (string) getenv('KDP_STORE_API_KEY'); if ($provider === 'paypal' && $key === '') { $client = (string) getenv('KDP_STORE_CLIENT_ID'); $clientSecret = (string) getenv('KDP_STORE_CLIENT_SECRET'); storeRequire(strlen($client) >= 8 && strlen($clientSecret) >= 8, 'PRIVATE_PAYPAL_CLIENT_REQUIRED'); $token = storeRequest($origin, 'POST', '/v1/oauth2/token', ['Accept: application/json', 'Content-Type: application/x-www-form-urlencoded'], 'grant_type=client_credentials', $client . ':' . $clientSecret); $key = (string) ($token['access_token'] ?? ''); storeRequire(($token['token_type'] ?? '') === 'Bearer', 'PAYPAL_TOKEN_NOT_CONFIRMED'); } $basic = null; $headers = ['Accept: application/json', 'Content-Type: application/json']; if ($provider === 'stripe') { storeRequire(str_starts_with($key, $sandbox ? 'sk_test_' : 'sk_live_'), 'PROVIDER_KEY_MODE_MISMATCH'); $headers[] = 'Stripe-Version: ' . $config['api_version']; } if ($provider === 'shopify') { $headers[] = 'X-Shopify-Access-Token: ' . $key; } elseif (in_array($provider, ['fastspring', 'woocommerce'], true)) { $basic = $key . ':' . (string) getenv('KDP_STORE_API_SECRET'); } elseif ($provider === 'gumroad') { /* Native API access_token parameter is appended privately below. */ } else { $headers[] = 'Authorization: Bearer ' . $key; } if ($provider === 'lemon-squeezy') { $headers = ['Accept: application/vnd.api+json', 'Content-Type: application/vnd.api+json', 'Authorization: Bearer ' . $key]; } storeRequire(strlen($key) >= 8, 'PRIVATE_PROVIDER_KEY_REQUIRED'); return static function (string $method, string $path, ?array $body = null) use ($origin, $provider, $key, $basic, $headers): array { if ($provider === 'gumroad') { $path .= (str_contains($path, '?') ? '&' : '?') . 'access_token=' . rawurlencode($key); } return storeRequest($origin, $method, $path, $headers, $body, $basic); }; } ``` ## Expected result Missing/invalid native proof is refused; provider reads use pinned origins, TLS verification, no redirects, bounded bodies and deadlines. Gumroad uses a merchant-authenticated sale lookup rather than an invented Ping signature. PayPal verifies through its own API rather than downloading an arbitrary certificate URL. These are reference controls, not an audit/certification or a guarantee against all attacks. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: shared decision contract Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/normalizer --- title: "Store service: shared decision contract" description: "Store service: shared decision contract for the complete private reference application." slug: "/docs/v1/guides/store-service/normalizer" section: "Webhooks and integrations" order: 793 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- This complete shared PHP normalizer loads the eight provider traits shown in their respective [guides](/docs/v1/guides/store-service#choose-your-provider). Download the [whole application](/docs/downloads/store-integrations.zip), not just this class. ## What you will learn Read and run the exact Store service: shared decision contract with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Complete source ```php explicit store decisions; included by service.php. // PHP 8.2+. A decision is not payment proof until authenticated provider API reads pass. require_once __DIR__ . '/providers/stripe.php'; require_once __DIR__ . '/providers/paddle.php'; require_once __DIR__ . '/providers/lemon.php'; require_once __DIR__ . '/providers/gumroad.php'; require_once __DIR__ . '/providers/shopify.php'; require_once __DIR__ . '/providers/woocommerce.php'; require_once __DIR__ . '/providers/paypal.php'; require_once __DIR__ . '/providers/fastspring.php'; final class StoreNormalizer { use StoreStripeNormalizer; use StorePaddleNormalizer; use StoreLemonNormalizer; use StoreGumroadNormalizer; use StoreShopifyNormalizer; use StoreWoocommerceNormalizer; use StorePaypalNormalizer; use StoreFastspringNormalizer; public function __construct(private array $config, private Closure $api, private Closure $lookup) {} private function get(string $path): array { return ($this->api)('GET', $path, null); } private function ref(mixed $id): string { storeRequire(is_string($id) || is_int($id), 'PROVIDER_REFERENCE_REQUIRED'); $id = (string) $id; storeRequire($id !== '' && strlen($id) <= 190, 'PROVIDER_REFERENCE_REQUIRED'); return rawurlencode($id); } private function mode(mixed $live): void { storeRequire(is_bool($live) && $live === ($this->config['environment'] === 'live'), 'PROVIDER_MODE_MISMATCH'); } private function one(array $items): array { storeRequire(count($items) === 1 && is_array($items[0]) && (int) ($items[0]['quantity'] ?? 1) === 1, 'SINGLE_UNIT_LINE_REQUIRED'); return $items[0]; } private function paid(string $order, string $receipt, string $offer, string $email, string $name, bool $recurring, array $links = [], ?string $initialKey = null, bool $initial = true): array { storeRequire(isset($this->config['offer_map'][$offer]) && filter_var($email, FILTER_VALIDATE_EMAIL) !== false, 'UNMAPPED_OFFER_OR_RECIPIENT'); return ['order' => $order, 'receipt' => $receipt, 'offer' => $this->config['offer_map'][$offer], 'customer' => ['email' => $email, 'name' => $name], 'state' => 'paid', 'recurring' => $recurring, 'links' => array_values(array_unique([$order, $receipt, ...$links])), 'initial_key' => $initialKey, 'initial' => $initial]; } private function negative(string $reference, string $receipt, string $state): array { $order = ($this->lookup)($reference); return ['order' => is_string($order) ? $order : $reference, 'receipt' => $receipt, 'state' => $state, 'links' => [], 'unmapped_reference' => is_string($order) ? null : $reference]; } public function normalize(array $authenticated, array $headers): array { return match ($this->config['provider']) { 'stripe' => $this->stripe($authenticated['native']), 'paddle' => $this->paddle($authenticated['native']), 'lemon-squeezy' => $this->lemon($authenticated['native']), 'gumroad' => $this->gumroad($authenticated['native']), 'shopify' => $this->shopify($authenticated['native'], $headers), 'woocommerce' => $this->woocommerce($authenticated['native'], $headers), 'paypal' => $this->paypal($authenticated['native']), 'fastspring' => $this->fastspring($authenticated['native']), default => throw new StoreRefusal('UNSUPPORTED_PROVIDER') }; } } ``` ## Decision boundary A paid decision carries an approved internal offer, authenticated recipient, stable order/payment receipt and initial-versus-renewal evidence. Negative decisions resolve the existing order through verified reference links. Unmatched adverse references are remembered for review. These normalized records are private application data, not a new KDPika public API, payment proof by themselves, or a universal provider schema. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting [license](/docs/v1/start/glossary#license) with the same buyer email; no full key appears in public replies. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: shared-hosting bootstrap source Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/bootstrap --- title: "Store service: shared-hosting bootstrap source" description: "Store service: shared-hosting bootstrap source for the complete private reference application." slug: "/docs/v1/guides/store-service/bootstrap" section: "Webhooks and integrations" order: 794 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Use only in a dedicated backend document root with private sibling state. Follow the complete [installation layout](/docs/v1/guides/store-service#install-the-files). Do not overwrite your existing store's public index or publish environment.php. ## What you will learn Read and run the exact Store service: shared-hosting bootstrap source with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Public entry point ```php $value) { if (!is_string($name) || !preg_match('/\AKDP_[A-Z0-9_]+\z/D', $name) || !is_string($value) || preg_match('/[\r\n\x00]/', $value)) { throw new RuntimeException('Invalid environment'); } putenv($name . '=' . $value); } require $private . '/stores/service.php'; } catch (Throwable) { http_response_code(503); header('Content-Type: application/json'); header('Cache-Control: no-store'); echo '{"accepted":false}'; } ``` ## Apache route file The complete stores/public/.htaccess file is included in the [source archive](/docs/downloads/store-integrations.zip); copy it only into this dedicated public directory. It disables directory listings and routes provider-event to index.php. For another web server configure the equivalent exact route; do not expose private source or state. ## Private CLI bootstrap ```php $value) { if (!is_string($name) || !preg_match('/\AKDP_[A-Z0-9_]+\z/D', $name) || !is_string($value) || preg_match('/[\r\n\x00]/', $value)) { throw new RuntimeException('Invalid environment'); } putenv($name . '=' . $value); } require __DIR__ . ($argv[1] === 'mail' ? '/mail.php' : '/service.php'); } catch (Throwable) { fwrite(STDERR, "Store configuration requires private review.\n"); exit(1); } ``` ## Expected result Only POST /provider-event invokes ingress; unknown routes return a safe refusal. The cron bootstrap requires PHP CLI and only accepts work, inspect or mail. It validates environment variable names/values and loads files outside the web root. Its safe error message is not permission to turn on display_errors publicly. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated [Sandbox](/docs/v1/start/glossary#sandbox) [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: Node.js native ingress Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/node --- title: "Store service: Node.js native ingress" description: "Store service: Node.js native ingress for the complete private reference application." slug: "/docs/v1/guides/store-service/node" section: "Webhooks and integrations" order: 795 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- This complete Node.js 18+ server relays native provider notifications to the [shared PHP store service](/docs/v1/guides/store-service). The PHP service remains required for provider verification, persistence, worker and mail. It is not a standalone Node payment worker or a Node requirement for PHP shared hosting. ## What you will learn Read and run the exact Store service: Node.js native ingress with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Configure and run Set private KDP_STORE_SERVICE_URL to the dedicated PHP service's HTTPS origin followed by /provider-event. Set optional KDP_STORE_RELAY_PORT; default 8118. Put your own HTTPS reverse proxy in front of this loopback listener and preserve raw bodies/headers. Register that public proxy URL with the provider. Do not use a redirect or expose any provider/API credential in JavaScript sent to a browser. ```bash node --check stores/relay.mjs node stores/relay.mjs ``` Expected: listening:true and loopback_only:true. Forward only POST /provider-event; a complete, verified upstream durable receipt returns 202 with accepted:true. A failed/oversized/invalid/uncertain upstream receipt returns a refusal, never false success. The worker cron runs independently on PHP hosting. ## Complete source ```javascript #!/usr/bin/env node // Optional Node18+ native HTTP ingress for the complete PHP store service. // This is NOT a pure Node payment worker: service.php authenticates and commits. // Set KDP_STORE_SERVICE_URL (HTTPS /provider-event), KDP_STORE_RELAY_PORT (8118). // Run: node relay.mjs. Expected: only verified durable upstream receipts return202. import http from 'node:http'; const target = new URL(process.env.KDP_STORE_SERVICE_URL ?? ''); const loopback = process.env.KDP_STORE_ENVIRONMENT === 'sandbox' && process.env.KDP_STORE_ALLOW_LOOPBACK === '1' && target.hostname === '127.0.0.1' && target.protocol === 'http:'; if ((target.protocol !== 'https:' && !loopback) || target.username || target.password || target.search || target.hash || target.pathname !== '/provider-event') throw new Error('Private service URL required'); const allowed = new Set(['content-type', 'stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time']); const reply = (response, status, body) => { response.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', 'X-Content-Type-Options': 'nosniff', Connection: 'close' }); response.end(body); }; const server = http.createServer((request, response) => { if (request.method !== 'POST' || request.url !== '/provider-event') return reply(response, 404, '{"accepted":false}'); if (!/^\d+$/.test(request.headers['content-length'] ?? '') || request.headers['transfer-encoding']) return reply(response, 400, '{"accepted":false}'); if (Number(request.headers['content-length']) > 262144) return reply(response, 413, '{"accepted":false}'); const headers = {}, seen = new Set(); for (let i = 0; i < request.rawHeaders.length; i += 2) { const name = request.rawHeaders[i].toLowerCase(); if (!allowed.has(name)) continue; if (seen.has(name)) return reply(response, 401, '{"accepted":false}'); seen.add(name); headers[name] = request.rawHeaders[i + 1]; } const chunks = []; let length = 0; request.on('data', chunk => { length += chunk.length; if (length <= 262144) chunks.push(chunk); }); request.on('error', () => response.destroy()); request.on('end', async () => { if (length !== Number(request.headers['content-length']) || length > 262144) return reply(response, 400, '{"accepted":false}'); try { const upstream = await fetch(target, { method: 'POST', headers, body: Buffer.concat(chunks), redirect: 'manual', signal: AbortSignal.timeout(12000) }); // Bound allocation while reading, not only after an arbitrarily large body. const reader = upstream.body?.getReader(); if (!reader) throw new Error('Missing receipt'); const parts = []; let received = 0; try { while (true) { const { done, value } = await reader.read(); if (done) break; received += value.byteLength; if (received > 4096) { await reader.cancel(); throw new Error('Invalid receipt'); } parts.push(Buffer.from(value)); } } finally { reader.releaseLock(); } const bytes = Buffer.concat(parts, received); const receipt = JSON.parse(bytes.toString('utf8')); if (upstream.status === 202 && receipt.accepted === true && typeof receipt.duplicate === 'boolean') return reply(response, 202, JSON.stringify({ accepted: true, duplicate: receipt.duplicate })); return reply(response, [400, 401, 409, 413, 415, 429].includes(upstream.status) ? upstream.status : 503, '{"accepted":false}'); } catch { return reply(response, 503, '{"accepted":false}'); } }); }); server.requestTimeout = 5000; server.headersTimeout = 5000; server.keepAliveTimeout = 1000; server.listen(Number(process.env.KDP_STORE_RELAY_PORT ?? 8118), '127.0.0.1', () => console.log('{"listening":true,"loopback_only":true}')); ``` ## Test and operating boundary This exact ingress was executed for all eight provider contracts and both one-time/recurring profiles where supplied. It preserves the original bytes and allowlisted authentication headers and refuses duplicate relevant headers. The response stream is bounded during reading. Local test overrides require [Sandbox](/docs/v1/start/glossary#sandbox) plus literal HTTP 127.0.0.1 and explicit consent flags; remove them in deployment. A public HTTPS proxy/merchant account was not used in these local contract tests. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting [license](/docs/v1/start/glossary#license) with the same buyer email; no full key appears in public replies. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Store service: Python native ingress Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/python --- title: "Store service: Python native ingress" description: "Store service: Python native ingress for the complete private reference application." slug: "/docs/v1/guides/store-service/python" section: "Webhooks and integrations" order: 796 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- This complete Python 3.11+ standard-library HTTP server relays native Stripe requests to the [shared PHP service](/docs/v1/guides/store-service). PHP remains the native verification, durable fulfillment and mail runtime. This is not an independent Python payment worker or a requirement for shared PHP hosting. ## What you will learn Read and run the exact Store service: Python native ingress with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Configure and run Set private KDP_STORE_SERVICE_URL to your PHP service's exact HTTPS /provider-event URL. KDP_STORE_RELAY_PORT defaults to 8119. Use your own HTTPS reverse proxy in front of this loopback development listener; register its exact public URL with Stripe and preserve raw bytes and relevant headers. ```bash python3 -m py_compile stores/relay.py python3 stores/relay.py ``` Expected: a loopback listener, then only a real accepted upstream durable receipt produces HTTP 202. A signature refusal, invalid/oversized reply, redirect or timeout is not acknowledged as success. Run the separate PHP cron for licensing/mail. ## Complete source ```python #!/usr/bin/env python3 """Optional Python3.11+ loopback ingress; complete PHP service is required. KDP_STORE_SERVICE_URL=https://YOUR_STORE_BACKEND/provider-event, optional port8119. Run python3 relay.py. Only the service verifies native payments and commits inbox. No external dependency, card collection, credential logging or blind retries. """ from http.server import BaseHTTPRequestHandler, HTTPServer import json import os import urllib.error import urllib.parse import urllib.request TARGET = os.environ['KDP_STORE_SERVICE_URL'] URL = urllib.parse.urlsplit(TARGET) LOCAL = os.environ.get('KDP_STORE_ENVIRONMENT') == 'sandbox' and os.environ.get('KDP_STORE_ALLOW_LOOPBACK') == '1' and URL.hostname == '127.0.0.1' and URL.scheme == 'http' if (URL.scheme != 'https' and not LOCAL) or URL.username or URL.password or URL.query or URL.fragment or URL.path != '/provider-event': raise SystemExit('Private service URL required') ALLOWED = {'content-type', 'stripe-signature', 'paddle-signature', 'x-signature', 'x-wc-webhook-signature', 'x-wc-webhook-topic', 'x-shopify-hmac-sha256', 'x-shopify-topic', 'x-shopify-shop-domain', 'x-shopify-webhook-id', 'x-fs-signature', 'paypal-auth-algo', 'paypal-cert-url', 'paypal-transmission-id', 'paypal-transmission-sig', 'paypal-transmission-time'} class NoRedirect(urllib.request.HTTPRedirectHandler): def redirect_request(self, request, response, code, message, headers, target): return None class Receiver(BaseHTTPRequestHandler): def log_message(self, *args): return def reply(self, code, value): raw = json.dumps(value, separators=(',', ':')).encode() self.send_response(code) self.send_header('Content-Type', 'application/json') self.send_header('Content-Length', str(len(raw))) self.send_header('Cache-Control', 'no-store') self.send_header('X-Content-Type-Options', 'nosniff') self.end_headers() self.wfile.write(raw) def do_POST(self): self.connection.settimeout(5) if self.path != '/provider-event': return self.reply(404, {'accepted': False}) lengths = self.headers.get_all('Content-Length', []) if self.headers.get('Transfer-Encoding') or len(lengths) != 1 or not lengths[0].isdigit(): return self.reply(400, {'accepted': False}) size = int(lengths[0]) if size > 262144: return self.reply(413, {'accepted': False}) headers = {} for name in ALLOWED: values = self.headers.get_all(name, []) if len(values) > 1: return self.reply(401, {'accepted': False}) if values: headers[name] = values[0] try: raw = self.rfile.read(size) if len(raw) != size: return self.reply(400, {'accepted': False}) request = urllib.request.Request(TARGET, method='POST', data=raw, headers=headers) try: result = urllib.request.build_opener(NoRedirect()).open(request, timeout=12) except urllib.error.HTTPError as error: result = error with result: body = result.read(4097) if len(body) > 4096: raise ValueError('Invalid receipt') receipt = json.loads(body) if result.status == 202 and receipt.get('accepted') is True and type(receipt.get('duplicate')) is bool: return self.reply(202, {'accepted': True, 'duplicate': receipt['duplicate']}) return self.reply(result.status if result.status in {400, 401, 409, 413, 415, 429} else 503, {'accepted': False}) except (OSError, ValueError, TypeError): return self.reply(503, {'accepted': False}) if __name__ == '__main__': print('{"listening":true,"loopback_only":true}', flush=True) with HTTPServer(('127.0.0.1', int(os.environ.get('KDP_STORE_RELAY_PORT', '8119'))), Receiver) as server: server.serve_forever() ``` ## Verification boundary The exact server was exercised with local Stripe one-time/subscription contracts and actual isolated [Sandbox](/docs/v1/start/glossary#sandbox) licensing. It is not a hosted merchant checkout or production reverse-proxy test. Optional literal-loopback HTTP overrides require Sandbox and an explicit flag; they are not production TLS exceptions. Keep all secrets and state on the private service, not on the buyer's desktop or browser. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting [license](/docs/v1/start/glossary#license) with the same buyer email; no full key appears in public replies. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider [delivery](/docs/v1/start/glossary#delivery) evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling [Live](/docs/v1/start/glossary#live). ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Deliver store license keys by email Canonical: https://licensing.kdpika.com/docs/v1/guides/store-service/email --- title: "Deliver store license keys by email" description: "Deliver store license keys by email for the complete private reference application." slug: "/docs/v1/guides/store-service/email" section: "Webhooks and integrations" order: 797 audience: "all" level: "advanced" last_updated: "2026-10-18" version: "v1" related: ["/docs/v1/guides/any-store", "/docs/v1/api/idempotency"] draft: false --- Use the exact private encrypted key saved after a confirmed initial issuance. The mail worker never calls create-license, so an email retry cannot mint a second key. This is your software buyer's [delivery](/docs/v1/start/glossary#delivery) workflow, not KDPika platform account email configuration. ## What you will learn Read and run the exact Deliver store [license keys](/docs/v1/start/glossary#license-key) by email with the complete shared application, without exposing private credentials or claiming an unverified payment. ## Configure private SMTP 1. Complete the [store service installation](/docs/v1/guides/store-service) and ensure the initial licensing effect is DONE. 2. In private environment.php set KDP_SMTP_HOST, KDP_SMTP_PORT, KDP_SMTP_USERNAME, KDP_SMTP_PASSWORD and KDP_SMTP_FROM for your own authenticated sender. 3. Use port 465 for implicit TLS or 587 for STARTTLS. Certificate validation remains enabled. Do not bypass TLS to make a production test pass. 4. Set KDP_SOFTWARE_NAME to your application's plain display name. The worker refuses newline injection in header-related values and sends a UTF-8 plain-text transactional email. 5. Verify your sending domain's SPF/DKIM and applicable DMARC policy with your mail provider. Use a sender address you control and test the actual recipient inbox/spam folder. ## Run and inspect ```bash /usr/local/bin/php /home/ACCOUNT/license-events/private/kdpika-store/stores/cron.php mail ``` Replace the CLI binary/path with your host's actual values. Expected after one completed issue: smtp_accepted:1, review:0, followed by smtp_accepted:0 on another run. The exact JSON also indicates any held items. The worker selects at most 25 previously unattempted saved keys and tracks the ciphertext fingerprint in a separate private mail.sqlite ledger. A large already-sent prefix cannot starve later deliveries. SMTP DATA 250 means the mail server explicitly accepted the message, not inbox delivery or reading. A disconnect after DATA, an unknown acknowledgement or interruption leaves IN_FLIGHT/REVIEW and does not auto-resend. Check your provider's private delivery evidence before a reviewed retry. No reset/resend automation is supplied. Do not delete mail.sqlite or create another [license](/docs/v1/start/glossary#license) to make delivery green. ## Complete source ```php authenticated SMTP delivery worker. // PHP 8.2+, PDO SQLite, OpenSSL; same private fulfillment/store configuration. // Run: php stores/mail.php. No keys/addresses/credentials are printed. // Unknown SMTP acknowledgement is held for review, never blindly resent. ini_set('display_errors', '0'); function mailRequire(bool $condition): void { if (!$condition) { throw new RuntimeException('Private mail configuration or delivery requires review'); } } function sendLicenseMail(string $recipient, string $key): void { $host = (string) getenv('KDP_SMTP_HOST'); $port = (int) getenv('KDP_SMTP_PORT'); $from = (string) getenv('KDP_SMTP_FROM'); $name = (string) getenv('KDP_SOFTWARE_NAME'); mailRequire(filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false && filter_var($from, FILTER_VALIDATE_EMAIL) !== false); mailRequire($name !== '' && strlen($name) <= 120 && !preg_match('/[\r\n]/', $name . $from . $recipient . $host)); $config = json_decode((string) file_get_contents((string) getenv('KDP_FULFILLMENT_CONFIG')), true, 64, JSON_THROW_ON_ERROR); $local = $config['environment'] === 'sandbox' && getenv('KDP_SMTP_ALLOW_LOOPBACK') === '1' && $host === '127.0.0.1'; mailRequire($local || in_array($port, [465, 587], true)); $context = stream_context_create(['ssl' => ['verify_peer' => true, 'verify_peer_name' => true, 'peer_name' => $host, 'allow_self_signed' => false]]); $stream = stream_socket_client(($port === 465 && !$local ? 'ssl://' : 'tcp://') . $host . ':' . $port, $code, $error, 10, STREAM_CLIENT_CONNECT, $context); mailRequire(is_resource($stream)); stream_set_timeout($stream, 10); $read = static function (int $expected) use ($stream): void { $bytes = 0; do { $line = fgets($stream, 2048); mailRequire(is_string($line)); $bytes += strlen($line); mailRequire($bytes <= 16384 && preg_match('/\A[0-9]{3}[- ]/', $line) === 1); $more = $line[3] === '-'; $status = (int) substr($line, 0, 3); } while ($more); mailRequire($status === $expected); }; $write = static function (string $line) use ($stream): void { $raw = $line . "\r\n"; mailRequire(fwrite($stream, $raw) === strlen($raw)); }; try { $read(220); $write('EHLO localhost'); $read(250); if (!$local && $port === 587) { $write('STARTTLS'); $read(220); mailRequire(stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) === true); $write('EHLO localhost'); $read(250); } if (!$local) { $username = (string) getenv('KDP_SMTP_USERNAME'); $password = (string) getenv('KDP_SMTP_PASSWORD'); mailRequire($username !== '' && $password !== ''); $write('AUTH LOGIN'); $read(334); $write(base64_encode($username)); $read(334); $write(base64_encode($password)); $read(235); } $write('MAIL FROM:<' . $from . '>'); $read(250); $write('RCPT TO:<' . $recipient . '>'); $read(250); $write('DATA'); $read(354); $subject = '=?UTF-8?B?' . base64_encode('Your ' . $name . ' license') . '?='; $message = 'From: ' . $from . "\r\nTo: " . $recipient . "\r\nSubject: " . $subject . "\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n" . 'Thank you for your purchase of ' . $name . ".\r\n\r\nYour software license key:\r\n" . $key . "\r\n\r\nKeep this key private. Use the purchase email when the application asks for it.\r\nFor help, reply to this email.\r\n"; $message = preg_replace('/(?m)^\./', '..', $message); $write($message . '.'); $read(250); // This is the SMTP acceptance boundary, not inbox/read proof. // QUIT failure after explicit DATA250 must not change a confirmed send into a resend. @fwrite($stream, "QUIT\r\n"); } finally { fclose($stream); } } function deliverLicenseOutbox(): array { umask(0077); $directory = (string) getenv('KDP_STORE_STATE'); $fulfillment = (string) getenv('KDP_FULFILLMENT_STATE'); foreach ([$directory, $fulfillment] as $path) { mailRequire(str_starts_with($path, '/') && is_dir($path) && !is_link($path) && (fileperms($path) & 0077) === 0); } $key = base64_decode((string) getenv('KDP_FULFILLMENT_OUTBOX_KEY'), true); mailRequire(is_string($key) && strlen($key) === 32); $lockPath = $directory . '/mail.lock'; mailRequire(!is_link($lockPath)); $lock = fopen($lockPath, 'c'); mailRequire($lock !== false && flock($lock, LOCK_EX | LOCK_NB)); try { $database = $directory . '/mail.sqlite'; mailRequire(!is_link($database)); $mail = new PDO('sqlite:' . $database, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]); chmod($database, 0600); $mail->exec('PRAGMA busy_timeout=3000'); $mail->exec('CREATE TABLE IF NOT EXISTS delivery(order_key TEXT PRIMARY KEY,cipher_hash TEXT NOT NULL,state TEXT NOT NULL)'); $source = new PDO('sqlite:' . $fulfillment . '/fulfillment.sqlite', null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]); $binding = $source->prepare('SELECT outbox_key_hash FROM binding WHERE id=1'); $binding->execute(); $bound = (string) $binding->fetchColumn(); $binding->closeCursor(); mailRequire(hash_equals($bound, hash('sha256', $key))); $attach = $source->prepare('ATTACH DATABASE ? AS delivery_state'); $attach->execute([$database]); $query = $source->prepare('SELECT b.order_key,b.cipher,o.email FROM outbox b JOIN orders o ON o.order_key=b.order_key LEFT JOIN delivery_state.delivery d ON d.order_key=b.order_key WHERE d.order_key IS NULL ORDER BY b.created_at,b.order_key LIMIT 25'); $query->execute(); $rows = $query->fetchAll(); $query->closeCursor(); $sent = 0; $review = 0; foreach ($rows as $row) { $find = $mail->prepare('SELECT cipher_hash,state FROM delivery WHERE order_key=?'); $find->execute([$row['order_key']]); $existing = $find->fetch(PDO::FETCH_ASSOC); $find->closeCursor(); $hash = hash('sha256', $row['cipher']); if ($existing) { mailRequire($existing['cipher_hash'] === $hash); continue; } $insert = $mail->prepare("INSERT INTO delivery(order_key,cipher_hash,state) VALUES(?,?,'IN_FLIGHT')"); $insert->execute([$row['order_key'], $hash]); try { $raw = base64_decode($row['cipher'], true); mailRequire(is_string($raw) && strlen($raw) >= 29); $plain = openssl_decrypt(substr($raw, 12, -16), 'aes-256-gcm', $key, OPENSSL_RAW_DATA, substr($raw, 0, 12), substr($raw, -16), $row['order_key']); mailRequire(is_string($plain)); sendLicenseMail($row['email'], $plain); unset($plain); $update = $mail->prepare("UPDATE delivery SET state='SENT' WHERE order_key=?"); $update->execute([$row['order_key']]); $sent++; } catch (Throwable) { $update = $mail->prepare("UPDATE delivery SET state='REVIEW' WHERE order_key=?"); $update->execute([$row['order_key']]); $review++; } } return ['smtp_accepted' => $sent, 'review' => $review]; } finally { flock($lock, LOCK_UN); fclose($lock); } } if (defined('KDP_STORE_MAIL_LIBRARY_ONLY') && KDP_STORE_MAIL_LIBRARY_ONLY === true) { return; } if (PHP_SAPI !== 'cli') { http_response_code(404); exit; } try { echo json_encode(deliverLicenseOutbox(), JSON_THROW_ON_ERROR) . PHP_EOL; } catch (Throwable) { fwrite(STDERR, "Mail state requires private review. No automatic resend.\n"); exit(1); } ``` ## Privacy and verification Keys are decrypted only for intended delivery using the private AES-256-GCM outbox key and order identity. Changing that key without a reviewed migration is refused. No full key, recipient or SMTP secret is printed. The message intentionally contains the buyer's secret license, so restrict mailbox access and never share email screenshots publicly. The exact source passed a local SMTP conversation for acceptance, duplicate prevention, lost acknowledgement, wrong encryption key and more than 100 existing sent entries. That explicitly allowed local [Sandbox](/docs/v1/start/glossary#sandbox) server is not a production SMTP TLS/certificate test or a genuine inbox-delivery claim. Run your real verified-TLS sender/inbox test before [Live](/docs/v1/start/glossary#live). For authenticated manual redelivery use [saved-key export](/docs/v1/guides/manual-issuance#export-a-saved-key-for-delivery), never a second create. ## Prerequisites Complete the [private service installation](/docs/v1/guides/store-service). Keep all files from the exact source archive together, use PHP 8.2+ with its documented extensions and start with an isolated Sandbox [product](/docs/v1/start/glossary#product)/key. A source fragment alone is not the application. ## Expected result Only a confirmed provider purchase authorizes a licensing effect. A durable HTTP receipt, completed licensing operation and SMTP acceptance are separate outcomes. Inspect aggregate worker states and verify the resulting license with the same buyer email; no full key appears in public replies. ## Common mistakes Do not deploy one module without its archive dependencies, place configuration under the web root, skip provider API verification or clear a durable ledger to bypass review. Optional Node/Python ingress still requires the PHP worker. ## Security notes Use least-privilege private provider and product-scoped licensing credentials, verified HTTPS, owner-only persistent state and separate secrets per account/environment. Never trust browser payment claims. Preserve held operations after an unknown result and reconcile before another write; exactly-once behavior across systems is not guaranteed. ## Troubleshooting For a receipt refusal check raw-byte preservation, the matching endpoint secret/account/mode, server clock where applicable and the private bootstrap layout. For REVIEW/IN_FLIGHT inspect provider state, safe private error codes and the licensing ledger before retrying. Missing permissions, recipient data or an initial mapping are not successful payments. For mail verify the separate SMTP ledger and provider delivery evidence. Do not post raw events, keys or SQLite files publicly. ## Next steps Run the selected [provider Sandbox procedure](/docs/v1/guides/store-service#choose-your-provider), then test [private key email delivery](/docs/v1/guides/store-service/email). Review [supported recipe limitations](/docs/v1/guides/recipes) before enabling Live. ## Related - [Generic store trust boundary](/docs/v1/guides/any-store) - [API idempotency](/docs/v1/api/idempotency) - [Private service setup](/docs/v1/guides/store-service) - [Manual key delivery](/docs/v1/guides/manual-issuance) # Previous Developer documentation reference Canonical: https://licensing.kdpika.com/docs/v1/help/previous-home --- order: 970 slug: "/docs/v1/help/previous-home" title: "Previous Developer documentation reference" section: "Help and notes" description: "Preserved detailed reference from the earlier home page, including its unique explanations and examples." audience: "all" level: "beginner" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"] draft: false layout: "reference" --- Build your application with a clear licensing contract, starting with a tested [Sandbox](/docs/v1/start/glossary#sandbox) integration. ## What you will learn - Find a first working [activation](/docs/v1/start/glossary#activation) and the right application architecture. - Understand the credential and environment boundaries. - Locate current references, examples and support without an account. ## Prerequisites - No account is needed to read documentation or use its browser search. - To execute requests, create a verified Sandbox workspace and test [license](/docs/v1/start/glossary#license). - See [how to read, search and export these docs](/docs/v1/documentation). ## Start here :::cards - [Quickstart in five minutes](/docs/v1/start/quickstart) - [Choose your application path](/docs/v1/start/choose-your-path) - [Understand core concepts](/docs/v1/start/core-concepts) - [Check known SDK limitations](/docs/v1/integration-notes) ::: Press Ctrl+K or Command+K to search published sections. Search is local to your browser; no account or external search service is required. Without JavaScript, use the sidebar and browser Find. ## Popular pages - [Authentication and secret handling](/docs/v1/start/authentication) - [Sandbox versus Live](/docs/v1/start/environments) - [License lifecycle](/docs/v1/start/license-lifecycle) - [Activation, validation and deactivation](/docs/v1/start/activation-validation-deactivation) - [Go live checklist](/docs/v1/start/go-live-checklist) - [Alphabetical glossary](/docs/v1/start/glossary) ## API reference The API uses the hosted /api/v1 base path. Begin with [credential selection](/docs/v1/start/authentication#headers-and-environment) and [request flow](/docs/v1/start/activation-validation-deactivation). These foundation pages explain actual behaviour; they are not an exhaustive endpoint reference. Only complete pages are linked. Do not treat an authenticated legacy export as a substitute for the disclosed compatibility notes. ## SDKs Four [official SDKs](/docs/v1/start/glossary#official-sdk) share the activation contract: - [Python 3.9+ setup](/docs/v1/start/quickstart#python-setup) - [JavaScript: Node.js 18+ / Electron main](/docs/v1/start/quickstart#javascript-setup) - [PHP 8.2+ setup](/docs/v1/start/quickstart#php-setup) - [C# / .NET source setup](/docs/v1/start/quickstart#csharp-setup) Other languages can use HTTPS and JSON. The [cURL transport check](/docs/v1/start/quickstart#curl-setup) is not a complete signature verifier. Consult [runtime and storage limitations](/docs/v1/integration-notes#runtime-and-storage) before production. ## Guides Choose [desktop](/docs/v1/start/use-cases#desktop-application), [SaaS backend](/docs/v1/start/use-cases#saas-backend), [CLI](/docs/v1/start/use-cases#cli-or-developer-tool) or [plugin/extension](/docs/v1/start/use-cases#plugin-or-extension). Your own sales backend handles software-buyer payments. Stripe and Mollie in KDPika checkout bill your [platform subscription](/docs/v1/start/glossary#platform-subscription), not those purchases. ## Changelog 2026-10-17: foundation guides, a glossary, annotated flow diagrams and executed online quickstarts are published. 2026-10-15: public documentation adds local section search, Markdown exports, themes and privacy-conscious feedback. These entries describe documentation changes, not software releases, certifications or uptime guarantees. ## Support Email [hi@kdpika.com](mailto:hi@kdpika.com) for help. Include your runtime, environment and a [request ID](/docs/v1/start/glossary#request-id) where available; remove passwords, tokens and private [customer](/docs/v1/start/glossary#customer) information. [Platform status](/status) shows recorded health information. Use the feedback form on a page to explain unclear documentation. It is not a private account-support channel. ## Expected result You can find the first activation, the relevant architecture and current limitations while signed out. ## Common mistakes - Looking for buyer payment checkout in a licensing guide: your sales flow is separate. - Treating a listed runtime minimum as a current vendor support guarantee. - Assuming an unpublished guide or unsupported capability exists. ## Security notes Do not paste credentials into search, feedback or issue-report emails. Public exports contain documentation, not your account data. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Search has no match | The term is absent from published content | Try a shorter phrase or browse the topic | | A future guide is not visible | Only completed content is public | Use the available foundation or contact support | | Cannot execute activation | No test license or SDK setup | Follow the quickstart in order | ## Next steps Start with [introduction](/docs/v1/start/introduction), or run the [quickstart](/docs/v1/start/quickstart). ## Related - [Documentation interface](/docs/v1/documentation) - [Integration limitations](/docs/v1/integration-notes) - [Public privacy notice](/legal/privacy) # Previous Quickstart in five minutes reference Canonical: https://licensing.kdpika.com/docs/v1/help/previous-quickstart --- order: 971 slug: "/docs/v1/help/previous-quickstart" title: "Previous Quickstart in five minutes reference" section: "Help and notes" description: "Preserved detailed reference from the earlier quickstart page, including its unique explanations and examples." audience: "all" level: "beginner" last_updated: "2026-10-21" version: "v1" related: ["/docs/v1/start/authentication","/docs/v1/start/activation-validation-deactivation"] draft: false layout: "reference" --- Run a complete [Sandbox](/docs/v1/start/glossary#sandbox) [activation](/docs/v1/start/glossary#activation), signed online validation, [feature](/docs/v1/start/glossary#feature) check and deactivation with an [official SDK](/docs/v1/start/glossary#official-sdk). This page preserves the earlier quickstart for reference. Its Python 1.0.0 install commands are legacy instructions, not the current recommendation. Use [the current quickstart](/docs/v1/start/quickstart) and [Python SDK 1.1.5](/docs/v1/sdks/python) for a new integration. ## What you will learn - Create a [product](/docs/v1/start/glossary#product), [product plan](/docs/v1/start/glossary#product-plan), feature and [customer](/docs/v1/start/glossary#customer) [license](/docs/v1/start/glossary#license). - Run Python, JavaScript, PHP or C# against the same API. - Verify that deactivation releases the active [device](/docs/v1/start/glossary#device) seat. ## Prerequisites - A verified account with an available Sandbox allowance. - One installed runtime from the setup sections below. - Internet access and permission to write your application-data directory. - Read [authentication](/docs/v1/start/authentication) and [integration limitations](/docs/v1/integration-notes). The integration targets five minutes once your account, runtime and SDK download are ready. Email [delivery](/docs/v1/start/glossary#delivery) and dependency installation can take longer; this is not a completion-time guarantee. ## Create the Sandbox records ### Verify your account 1. Open [registration](/register), select Free for evaluation and submit an email you control. 2. Use the message's six-digit code or secure link. 3. Complete your name, workspace, password and terms acknowledgement. Expected result: a Sandbox workspace opens after email proof. Paid selections require confirmed platform payment, not automatic conversion to Free. ![Select Free, verify the email code or link, then complete the Sandbox workspace. No usable account precedes email proof.](/docs/diagrams/p4-setup-account.svg) Select Free, verify the email code or link, then complete the Sandbox workspace. No usable account precedes email proof. ### Create your product 1. Select Sandbox in the workspace header. 2. Open Products and choose New product or Create product. 3. Enter Product name Quickstart App, Slug quickstart-app, Platform Desktop and License prefix QUICK. 4. Choose Create product. 5. Copy the [public App ID](/docs/v1/start/glossary#public-app-id) shown on the product card. Expected result: the card identifies your application. The public App ID is not a secret management key. ![Products: Quickstart App, quickstart-app, Desktop, prefix QUICK. Save, then copy the public App ID on the card.](/docs/diagrams/p4-setup-product.svg) Products: Quickstart App, quickstart-app, Desktop, prefix QUICK. Save, then copy the public App ID on the card. ### Create your plan and feature 1. Open Plans & features and choose New plan or Create plan. 2. Select Quickstart App; enter Plan name Quickstart plan and Slug quickstart-plan. 3. Set Price 19.50, Currency EUR, Billing type Monthly and Duration days 30. 4. Set Max devices 2 and Feature keys pdf_export. 5. Choose Create plan. Expected result: the plan enables the boolean pdf_export feature. This product-plan price is metadata; it does not charge anyone. ![Plans and features: Quickstart plan, EUR 19.50, Monthly, 30 days, two devices. Feature keys contains pdf_export. Save creates the enabled feature.](/docs/diagrams/p4-setup-plan.svg) Plans and features: Quickstart plan, EUR 19.50, Monthly, 30 days, two devices. Feature keys contains pdf_export. Save creates the enabled feature. :::warning Current SDK compatibility Keep the stated demonstration values. JavaScript 1.0.0 can reject signatures with integral floats such as a plan price of 19.00. C# 1.0.0 can reject reserialized Unicode caches. These are known defects, not invalid licenses. This quickstart tests fresh online validation, not reliable offline behaviour. Never disable verification; review the integration notes before production. ::: ### Issue a test license 1. Open Licenses in Sandbox and choose New license or Create license. 2. Select Quickstart App / Quickstart plan. 3. Enter your test Customer email and an ASCII Customer name such as Quickstart Tester. 4. Choose Create license. 5. Copy the one-time full [license key](/docs/v1/start/glossary#license-key) and keep it private. Expected result: the active license has zero [active devices](/docs/v1/start/glossary#active-devices). The associated customer is created or reused automatically. ![Sandbox Licenses: select the product and plan, enter the test customer email and create. Copy the one-time license key; subsequent listings show a mask.](/docs/diagrams/p4-setup-license.svg) Sandbox Licenses: select the product and plan, enter the test customer email and create. Copy the one-time license key; subsequent listings show a mask. ## Download your SDK 1. Open Integration in your signed-in workspace. 2. Select the product, language and application type. 3. Download the matching version 1.0.0 SDK archive. Expected result: kdpika-python-sdk-1.0.0.tar.gz, kdpika-javascript-sdk-1.0.0.tar.gz, kdpika-php-sdk-1.0.0.tar.gz or kdpika-csharp-sdk-1.0.0.tar.gz. No public package-registry publication is assumed. ![Integration: select product and language, then download the official SDK. Use the public App ID in the client, never a developer API key.](/docs/diagrams/p4-setup-integration.svg) Integration: select product and language, then download the official SDK. Use the public App ID in the client, never a developer API key. ## Configure your environment Bash commands below use visible example identifiers. Replace them privately; read the actual license key through a hidden prompt to avoid command-history exposure. ```bash export KDP_APP_ID="YOUR_PUBLIC_APP_ID" export KDP_CUSTOMER_EMAIL="YOUR_TEST_CUSTOMER_EMAIL" export KDP_BASE_URL="https://licensing.kdpika.com" read -r -s -p "Sandbox license key: " KDP_LICENSE_KEY printf '\n' export KDP_LICENSE_KEY ``` Expected result: the variables exist in this terminal and its children. Do not print them. A supplied customer email must match the license; key-only activation is also supported. The command transcript uses Bash on macOS/Linux. On Windows, run it in a Bash environment with the chosen runtime installed there; this is not a native PowerShell transcript. The language samples use the SDK's OS-specific storage paths, but native Windows packaging and credential backends need separate testing. The samples explicitly use SANDBOX. KDP_BASE_URL defaults to the hosted HTTPS origin. Only an isolated local server may use loopback HTTP; never use remote plain HTTP. ## Choose a runtime Copy the complete files below and save them with the displayed filenames. Run only your chosen runtime. Installation commands run in the folder containing the downloaded archive and sample. ### Python setup Requires Python 3.9+, SDK 1.0.0 and the tested cryptography 46.0.3 dependency. Save python.py, then run: ```bash python3 -m venv .venv .venv/bin/python -m pip install cryptography==46.0.3 ./kdpika-python-sdk-1.0.0.tar.gz .venv/bin/python python.py ``` ```python """Sandbox activation, signed validation and deactivation. Requires Python 3.11–3.14 and kdpika-licensing 1.1.5 from PyPI. Run: python3 python.py with KDP_APP_ID and KDP_LICENSE_KEY in the environment. Expected: activation, validated pdf_export state, then released device seat. """ import os import sys from pathlib import Path from kdpika_licensing import LicenseClient def required(name): value = os.environ.get(name, "").strip() if not value: raise ValueError("Set " + name + " before running this sample.") return value def accepted(result): if not result.valid: actions = { "INVALID_LICENSE": "Check the key, email, App ID and Sandbox selection.", "DEVICE_LIMIT_REACHED": "Deactivate an old installation before retrying.", "ACTIVATION_LIMIT_REACHED": "Ask the publisher to review activation limits.", "NETWORK_ERROR": "Connect to the internet and retry.", "SIGNATURE_INVALID": "Stop and review the SDK compatibility notes.", } print("Stopped: " + str(result.error_code) + ". " + actions.get(result.error_code, "Review the license status and request ID."), file=sys.stderr) if result.request_id: print("Request ID: " + result.request_id, file=sys.stderr) raise SystemExit(1) return result def main(): state = os.environ.get("KDP_STATE_DIR") client = LicenseClient(product=required("KDP_APP_ID"), environment="SANDBOX", base_url=os.environ.get("KDP_BASE_URL", "https://licensing.kdpika.com"), application_name="KDPika Quickstart", application_version="1.0.0", storage_dir=Path(state) if state else None, prefer_keyring=False) accepted(client.activate(required("KDP_LICENSE_KEY"), os.environ.get("KDP_CUSTOMER_EMAIL") or None)) print("Activation accepted; signature verified.") result = accepted(client.validate()) print("Validation accepted; pdf_export=" + ("enabled." if result.feature("pdf_export") else "disabled.")) accepted(client.deactivate()) print("Deactivation accepted; device seat released.") if __name__ == "__main__": try: main() except Exception: print("Stopped: check configuration, storage permissions and connectivity. Do not disable signature checks.", file=sys.stderr) sys.exit(1) ``` The demo explicitly uses permission-protected files with prefer_keyring=False. That choice is reproducible, not an assertion of native credential-store security. ### JavaScript setup Requires Node.js 18+ and the local SDK 1.0.0. Save both files, then run: ```bash tar -xzf kdpika-javascript-sdk-1.0.0.tar.gz npm install --ignore-scripts node javascript.js ``` ```json { "name": "kdpika-quickstart-sample", "private": true, "type": "module", "engines": {"node": ">=18"}, "dependencies": {"@kdpika/license-sdk": "file:./kdpika-javascript-sdk"} } ``` ```javascript /** Sandbox activation, signed validation and deactivation. * Requirements: Node.js 18+, @kdpika/license-sdk 1.0.0 local package. * Run: node javascript.js with KDP_APP_ID and KDP_LICENSE_KEY set. * Expected: activation, validated pdf_export state, then released device seat. */ import {LicenseClient} from '@kdpika/license-sdk'; function required(name) { const value = (process.env[name] || '').trim(); if (!value) throw new Error(`Set ${name} before running this sample.`); return value; } function accepted(result) { if (!result.valid) { const actions = { INVALID_LICENSE:'Check the key, email, App ID and Sandbox selection.', DEVICE_LIMIT_REACHED:'Deactivate an old installation before retrying.', ACTIVATION_LIMIT_REACHED:'Ask the publisher to review activation limits.', NETWORK_ERROR:'Connect to the internet and retry.', SIGNATURE_INVALID:'Stop and review the SDK compatibility notes.' }; console.error(`Stopped: ${result.errorCode}. ${actions[result.errorCode] || 'Review the license status and request ID.'}`); if (result.requestId) console.error(`Request ID: ${result.requestId}`); process.exitCode = 1; throw new Error('License check rejected.'); } return result; } try { const client = new LicenseClient({product:required('KDP_APP_ID'), environment:'SANDBOX', baseUrl:process.env.KDP_BASE_URL || 'https://licensing.kdpika.com', applicationName:'KDPika Quickstart', applicationVersion:'1.0.0', storageDir:process.env.KDP_STATE_DIR}); accepted(await client.activate(required('KDP_LICENSE_KEY'), process.env.KDP_CUSTOMER_EMAIL || null)); console.log('Activation accepted; signature verified.'); const result = accepted(await client.validate()); console.log(`Validation accepted; pdf_export=${result.feature('pdf_export') ? 'enabled.' : 'disabled.'}`); accepted(await client.deactivate()); console.log('Deactivation accepted; device seat released.'); } catch { console.error('Stopped: check configuration, storage permissions and connectivity. Do not disable signature checks.'); process.exitCode = 1; } ``` Expected installation result: the local package is available to the ES module. Use Node or Electron main, not browser-delivered JavaScript. ### PHP setup Requires PHP 8.2+ with cURL and native Sodium. Save php.php, then run: ```bash tar -xzf kdpika-php-sdk-1.0.0.tar.gz export KDP_SDK_DIR="$PWD/kdpika-php-sdk" php php.php ``` ```php valid) { $action = match($result->errorCode) { 'INVALID_LICENSE' => 'Check the key, email, App ID and Sandbox selection.', 'DEVICE_LIMIT_REACHED' => 'Deactivate an old installation before retrying.', 'ACTIVATION_LIMIT_REACHED' => 'Ask the publisher to review activation limits.', 'NETWORK_ERROR' => 'Connect to the internet and retry.', 'SIGNATURE_INVALID' => 'Stop and review the SDK compatibility notes.', default => 'Review the license status and request ID.', }; fwrite(STDERR, 'Stopped: '.$result->errorCode.'. '.$action.PHP_EOL); if ($result->requestId) fwrite(STDERR, 'Request ID: '.$result->requestId.PHP_EOL); exit(1); } return $result; } try { $sdk = required('KDP_SDK_DIR'); require $sdk.'/src/LicenseResult.php'; require $sdk.'/src/LicenseClient.php'; $client = new LicenseClient(product:required('KDP_APP_ID'), environment:'SANDBOX', baseUrl:getenv('KDP_BASE_URL') ?: 'https://licensing.kdpika.com', applicationName:'KDPika Quickstart', applicationVersion:'1.0.0', storageDir:getenv('KDP_STATE_DIR') ?: null); accepted($client->activate(required('KDP_LICENSE_KEY'), getenv('KDP_CUSTOMER_EMAIL') ?: null)); echo 'Activation accepted; signature verified.', PHP_EOL; $result = accepted($client->validate()); echo 'Validation accepted; pdf_export=', $result->feature('pdf_export') ? 'enabled.' : 'disabled.', PHP_EOL; accepted($client->deactivate()); echo 'Deactivation accepted; device seat released.', PHP_EOL; } catch (Throwable) { fwrite(STDERR, 'Stopped: check configuration, storage permissions and connectivity. Do not disable signature checks.'.PHP_EOL); exit(1); } ``` The two source files load directly without a public Composer package. PHP 8.2 is required by the readonly result class despite the lower package metadata minimum. ### CSharp setup Requires a .NET 7-compatible SDK and local KDPika SDK 1.0.0. BouncyCastle.Cryptography is pinned to 2.7.0 in the SDK project. Save both files, then run: ```bash tar -xzf kdpika-csharp-sdk-1.0.0.tar.gz dotnet restore Quickstart.csproj dotnet run --project Quickstart.csproj --no-restore ``` ```xml Exenet7.0enableenablefalse ``` ```csharp // Purpose: Sandbox activation, signed online validation and deactivation. // Requires .NET 7 SDK, KDPika SDK 1.0.0, BouncyCastle.Cryptography 2.7.0. // Run: dotnet run --project Quickstart.csproj with KDP_APP_ID/KDP_LICENSE_KEY set. // Expected: activation, validated pdf_export state, then released device seat. using KDPika.LicenseSdk; string Required(string name) => Environment.GetEnvironmentVariable(name)?.Trim() is {Length: > 0} value ? value : throw new ArgumentException("Set " + name + " before running this sample."); LicenseResult Accepted(LicenseResult result) { if (!result.Valid) { var action = result.ErrorCode switch { "INVALID_LICENSE" => "Check the key, email, App ID and Sandbox selection.", "DEVICE_LIMIT_REACHED" => "Deactivate an old installation before retrying.", "ACTIVATION_LIMIT_REACHED" => "Ask the publisher to review activation limits.", "NETWORK_ERROR" => "Connect to the internet and retry.", "SIGNATURE_INVALID" => "Stop and review the SDK compatibility notes.", _ => "Review the license status and request ID." }; Console.Error.WriteLine($"Stopped: {result.ErrorCode}. {action}"); if (result.RequestId is not null) Console.Error.WriteLine("Request ID: " + result.RequestId); throw new InvalidOperationException("License check rejected."); } return result; } try { var client = new LicenseClient(product:Required("KDP_APP_ID"), environment:"SANDBOX", baseUrl:Environment.GetEnvironmentVariable("KDP_BASE_URL") ?? "https://licensing.kdpika.com", applicationName:"KDPika Quickstart", applicationVersion:"1.0.0", storageDir:Environment.GetEnvironmentVariable("KDP_STATE_DIR")); Accepted(await client.ActivateAsync(Required("KDP_LICENSE_KEY"), Environment.GetEnvironmentVariable("KDP_CUSTOMER_EMAIL"))); Console.WriteLine("Activation accepted; signature verified."); var result = Accepted(await client.ValidateAsync()); Console.WriteLine("Validation accepted; pdf_export=" + (result.Feature("pdf_export") ? "enabled." : "disabled.")); Accepted(await client.DeactivateAsync()); Console.WriteLine("Deactivation accepted; device seat released."); } catch { Console.Error.WriteLine("Stopped: check configuration, storage permissions and connectivity. Do not disable signature checks."); Environment.ExitCode = 1; } ``` Use a vendor-supported runtime for production; minimum source compatibility is not a security-support promise. Fresh online validation does not prove cached Unicode payloads work. ### cURL setup Requires Bash 3.2+, cURL 7.55+ and Python 3.9+ standard-library JSON handling. Save curl.sh and run: ```bash bash curl.sh ``` ```bash #!/usr/bin/env bash # Purpose: Sandbox activate/validate/deactivate HTTP check, not a signed verifier. # Requires Bash 3.2+, cURL 7.55+, Python 3.9+ standard library. No SDK needed. # Run: bash curl.sh with KDP_APP_ID and KDP_LICENSE_KEY set. # Expected: three accepted HTTP operations; no keys/tokens printed or persisted. set -euo pipefail set +x python3 - <<'PY' import json, os, subprocess, uuid from urllib.parse import urlsplit def required(name): value = os.environ.get(name, '').strip() if not value: raise SystemExit('Set '+name+' before running this sample.') if '\n' in value or '\r' in value: raise SystemExit('Invalid value for '+name) return value base = os.environ.get('KDP_BASE_URL', 'https://licensing.kdpika.com').rstrip('/') u = urlsplit(base) if u.username or u.password or u.query or u.fragment or u.path: raise SystemExit('Use an origin URL without credentials, query or path.') if u.scheme != 'https' and not (u.scheme == 'http' and u.hostname in ('127.0.0.1','localhost','::1')): raise SystemExit('Use HTTPS; loopback HTTP is allowed for local tests.') def request(action, body, headers): # cURL reads headers and JSON from stdin config, not secret-bearing argv/files. config = '\n'.join('header = '+json.dumps(k+': '+v) for k,v in headers.items()) config += '\ndata = '+json.dumps(json.dumps(body))+'\n' process = subprocess.run(['curl','--silent','--show-error','--connect-timeout','10', '--max-time','20','--config','-','--url',base+'/api/v1/licenses/'+action, '--write-out','\n%{http_code}'],input=config,text=True,capture_output=True,timeout=25) if process.returncode: raise SystemExit('Connection failed; check the URL and network.') try: raw,status = process.stdout.rsplit('\n',1) result = json.loads(raw) except (ValueError,TypeError): raise SystemExit('The server returned an invalid response.') if status != '200' or result.get('success') is not True: code = result.get('error',{}).get('code','INVALID_RESPONSE') raise SystemExit(action+' rejected: '+code+'; check credentials, environment, license status and limits.') return result identity = 'quickstart-'+str(uuid.uuid4()) body = {'license_key':required('KDP_LICENSE_KEY'),'device_id':identity, 'device_name':'Quickstart cURL','platform':'cli','app_version':'1.0.0'} if os.environ.get('KDP_CUSTOMER_EMAIL'): body['email'] = os.environ['KDP_CUSTOMER_EMAIL'] response = request('activate',body,{'Content-Type':'application/json', 'X-App-ID':required('KDP_APP_ID'),'X-Environment':'SANDBOX'}) token = response.get('activation_token','') if not token.startswith('act_'): raise SystemExit('Activation response is incomplete.') print('Activation HTTP accepted; signature not verified by this transport sample.') headers = {'Content-Type':'application/json','Authorization':'Bearer '+token,'X-Installation-ID':identity} request('validate',{},headers) print('Validation HTTP accepted.') request('deactivate',{},headers) print('Deactivation HTTP accepted; device seat released.') print('Transport test only. Use a verified SDK result before unlocking application features.') PY ``` Expected output: ```text Activation HTTP accepted; signature not verified by this transport sample. Validation HTTP accepted. Deactivation HTTP accepted; device seat released. Transport test only. Use a verified SDK result before unlocking application features. ``` :::security Transport is not verification This cURL check does not verify Ed25519, signed deadlines or installation binding. Do not unlock a paid feature from its HTTP success flag. The official SDK examples verify their signed results first. ::: ## Expected result Each official SDK prints: ```text Activation accepted; signature verified. Validation accepted; pdf_export=enabled. Deactivation accepted; device seat released. ``` 1. Open Devices and inspect the test installation. 2. Confirm its status is Deactivated. 3. Open Licenses and confirm the active device count returned to zero. Expected result: the seat is available again. Cumulative activation history is not reset. Reactivating a deactivated installation consumes another cumulative activation when such a maximum is configured. ![Devices shows Deactivated and Licenses shows zero active devices after the run. Releasing a seat does not reset cumulative activations.](/docs/diagrams/p4-setup-devices.svg) Devices shows Deactivated and Licenses shows zero active devices after the run. Releasing a seat does not reset cumulative activations. ## Common mistakes - Using an internal product record ID instead of the public app_ identifier. - Supplying a different customer email or the wrong environment. - Forgetting EUR when the current product-plan form defaults to USD. - Reading features before valid or Valid. - Treating fresh online success as a test of every cache, OS or numeric value. ## Security notes The examples do not print license keys, tokens or complete customer payloads. Never commit real credentials. Local state needs appropriate OS protection. If validation fails midway, a Sandbox seat can remain active. Inspect Devices and deactivate that installation deliberately; do not reset unrelated devices. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | INVALID_LICENSE | Key, App ID, Sandbox or supplied email mismatch | Compare all four privately | | DEVICE_LIMIT_REACHED | Active seat cap reached | Deactivate an old test installation | | ACTIVATION_LIMIT_REACHED | Cumulative cap reached | Ask the publisher to review policy; device reset does not clear the count | | SIGNATURE_INVALID | Canonicalization issue, altered data or untrusted key | Stop; review compatibility notes; never bypass verification | | pdf_export=disabled | Feature missing from selected plan | Inspect the product plan and correct the test license | | Import or assembly not found | Dependency installed in another project/runtime | Repeat installation in this sample directory | | Network error | Connectivity or origin mismatch | Check the URL and network | ## Next steps Read the [activation flow](/docs/v1/start/activation-validation-deactivation), then adapt your [use-case architecture](/docs/v1/start/use-cases). ## Related - [Authentication](/docs/v1/start/authentication) - [Environments](/docs/v1/start/environments) - [Go live checklist](/docs/v1/start/go-live-checklist) # Previous Choose your path reference Canonical: https://licensing.kdpika.com/docs/v1/help/previous-choose-your-path --- order: 972 slug: "/docs/v1/help/previous-choose-your-path" title: "Previous Choose your path reference" section: "Help and notes" description: "Preserved detailed reference from the earlier choose-your-path page, including its unique explanations and examples." audience: "all" level: "beginner" last_updated: "2026-10-17" version: "v1" related: ["/docs/v1/start/quickstart","/docs/v1/start/glossary"] draft: false layout: "reference" --- Choose where your application must enforce access before choosing a language or credential. ## What you will learn - Which process should activate and validate a [license](/docs/v1/start/glossary#license). - Which starter guide matches your application. - Where secret management credentials are allowed. ## Prerequisites - Read the [introduction](/docs/v1/start/introduction). - Know whether the protected operation runs on your server or the [customer](/docs/v1/start/glossary#customer)'s machine. ## Choose by application | Your application | Enforcement location | Begin here | | --- | --- | --- | | Desktop app | Application process; Electron main process for JavaScript | [Desktop architecture](/docs/v1/start/use-cases#desktop-application) and [quickstart](/docs/v1/start/quickstart) | | SaaS backend | Your authenticated server, before protected actions | [SaaS architecture](/docs/v1/start/use-cases#saas-backend) and [PHP setup](/docs/v1/start/quickstart#php-setup) | | CLI or [developer](/docs/v1/start/glossary#developer) tool | Tool process with protected local state; server checks for online resources | [CLI architecture](/docs/v1/start/use-cases#cli-or-developer-tool) and [Python setup](/docs/v1/start/quickstart#python-setup) | | Plugin or extension | Plugin process for local [features](/docs/v1/start/glossary#feature); your backend for valuable online operations | [Plugin architecture](/docs/v1/start/use-cases#plugin-or-extension) and [authentication](/docs/v1/start/authentication) | ## Choose by language - Python 3.9+: [Python quickstart](/docs/v1/start/quickstart#python-setup). - Node.js 18+ or Electron main process: [JavaScript quickstart](/docs/v1/start/quickstart#javascript-setup). - PHP 8.2+ with cURL and Sodium: [PHP quickstart](/docs/v1/start/quickstart#php-setup). - C# with .NET 7+ source compatibility: [C# quickstart](/docs/v1/start/quickstart#csharp-setup). - Another language: start with the [cURL transport check](/docs/v1/start/quickstart#curl-setup) and the [activation flow](/docs/v1/start/activation-validation-deactivation). A transport check is not a complete signed/offline client. ## Choose by responsibility If you develop the application, implement signed validation and feature gating. If you operate the sales backend, use a scoped [developer API key](/docs/v1/start/glossary#developer-api-key) for license management after confirmed purchases. If you support customers, use the dashboard to inspect a license and its [devices](/docs/v1/start/glossary#device) instead of asking for passwords. A browser-only feature check does not protect your SaaS backend. A customer-controlled plugin host is not your trusted management server. ## Expected result You have one starting guide and know where [activation tokens](/docs/v1/start/glossary#activation-token), installation state and developer API keys belong. ## Common mistakes - Running the JavaScript SDK in browser code: use Node or Electron main, or your authenticated backend. - Using an environment switch to migrate a license: [Sandbox](/docs/v1/start/glossary#sandbox) and [Live](/docs/v1/start/glossary#live) records are separate. - Assuming every listed REST language has a maintained SDK: four [official SDKs](/docs/v1/start/glossary#official-sdk) exist; other languages need a conforming client. ## Security notes Check the [known SDK limitations](/docs/v1/integration-notes) before choosing a production architecture. Runtime minimums describe source compatibility, not ongoing security support from the runtime vendor. ## Troubleshooting | Symptom | Cause | Fix | | --- | --- | --- | | Your app has both desktop and SaaS features | Enforcement occurs in two places | Gate local actions locally and authenticate/authorize online actions on your backend | | Your language has no official SDK | REST support is distinct from an SDK | Implement HTTPS/JSON plus signed semantic checks; do not copy only transport code | | The application must never connect | Online-first activation does not fit | Confirm requirements with support before purchase | ## Next steps Run the [quickstart](/docs/v1/start/quickstart) and review [use cases](/docs/v1/start/use-cases). ## Related - [Core concepts](/docs/v1/start/core-concepts) - [Environments](/docs/v1/start/environments) - [Go live checklist](/docs/v1/start/go-live-checklist)