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

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
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup><Compile Remove="errors/**/*.cs" /><None Remove="errors/**/obj/**;errors/**/bin/**" /></ItemGroup>
</Project>
```

## 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)
