Issue a 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.
- Read API conventions and authentication.
- Keep 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 licenses:write. The key fixes the organization/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 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. | "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
{
"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 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.
{
"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"] | 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.
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 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. Creates audit license.created and queues 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 before granting access.
cURL
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 sh request.sh create-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.
#!/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
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 |
|---|---|---|
| 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 and signed responses before integrating a purchase-to-license workflow.