Read a scoped unsigned 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.
- Read API conventions and authentication.
- Keep 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 response, a list operation or a replacement for signed installation validation.
Authentication and environment
Use Authorization: Bearer YOUR_DEVELOPER_API_KEY with scope licenses:read. The key fixes the organization/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 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 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.
{
"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 or 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"] | 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 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. | "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.
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 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/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 before granting access.
Python
Save every file shown below. Install the exact runtime/dependencies from the complete example archive README. Set the endpoint request JSON in KDP_BODY_JSON and the documented KDP_* credential variables in your private process environment.
Run python3 request.py get-license. Expected: full JSON, exit 0 for success, exit 1 for an API rejection; exit 2 for setup/transport failure. The cURL sample checks HTTP status; parse JSON before use. Never authorize a feature from transport output alone.
"""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)
Expected result
A correctly authenticated eligible request returns the documented HTTP status and schema. Check the actual success/error result and request IDs; a successful transport or unsigned management response does not grant a feature.
Common mistakes
- Mixing Public App IDs, internal product IDs and product-plan IDs.
- Assuming a mutation timeout means no work committed, or assuming Idempotency-Key protects every action.
- Logging raw responses that contain a newly issued license key or activation token.
Security notes
Keep lmp_ credentials out of shipped desktop/browser code. Verify signed payload bytes and product/environment/installation/status/time/feature policy. The examples do not fix the documented idempotency, cache canonicalization or current-key-only rotation limitations.
Troubleshooting
| 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 and signed responses before integrating a purchase-to-license workflow.