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.
- Complete the Sandbox quickstart.
Current licensing limits
| Surface | Bucket scope | Allowance / window | Headers |
|---|---|---|---|
| Public key | IP | 60 requests /60 seconds | Remaining/Reset on success |
| Activation | SHA256(IP + App ID + supplied key text) | 20 /60 seconds | Remaining/Reset on success |
| Validation | SHA256(IP + activation token) | 120 /60 seconds | Remaining/Reset on success |
| Developer pre-authentication | IP | 240 /60 seconds | Not reported separately |
| Create 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 | 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.
"""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 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/environment/installation/status/time/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, 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 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 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 and signature verification.