KDPika Licensing Sign inStart free
API v1
Show all languages
Browse documentation

Choose a page in the navigation drawer.

KDPika Licensing · API v1 Intermediate

Rate limits and bounded retries

Use exact licensing API windows and headers, avoid request loops, and distinguish rate limits from monthly usage allowances.

On this page
Updated 2026-10-17 UTCDownload Markdown

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

Current licensing limits

SurfaceBucket scopeAllowance / windowHeaders
Public keyIP60 requests /60 secondsRemaining/Reset on success
ActivationSHA256(IP + App ID + supplied key text)20 /60 secondsRemaining/Reset on success
ValidationSHA256(IP + activation token)120 /60 secondsRemaining/Reset on success
Developer pre-authenticationIP240 /60 secondsNot reported separately
Create licenseDeveloper key ID + license-create60 /60 secondsRemaining/Reset on success
Read licenseDeveloper key ID + license-read180 /60 secondsRemaining/Reset on success
Revoke/suspend/restore/renewDeveloper key ID + shared license-write60 /60 seconds sharedRemaining/Reset on success
Reset devicesDeveloper key ID + device-write60 /60 secondsRemaining/Reset on success
DeactivateNo dedicated limiter in current handlerNo fixed allowance can be promisedX-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

HeaderWhen presentInterpretation
X-Request-IDRouted success/errorCurrent correlation ID
X-RateLimit-RemainingSuccessful limited operationInteger remaining in the reached operation bucket
X-RateLimit-ResetSuccessful limited operationUnix seconds, not milliseconds or ISO text
Retry-AfterNot added by licensing errorsDo not assume a retry delay from a different billing route
X-RateLimit-LimitNot implemented by these handlersUse 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.

backoff.pypython
"""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

SymptomCauseAction
429 without delay headerCurrent licensing error behaviorWait a full window with jitter
Shared actions throttle unexpectedlyOne license-write bucket covers four actionsCoordinate management workers
Monthly usage exceeds displayAdvisory counter is not a hard rejectionMonitor account usage and choose a suitable plan

Next steps

Review the endpoint reference and signature verification.

Was this page helpful?

Choose an answer

Do not include personal data, license keys or credentials. Retained for up to 30 days. This is not a support inbox.

Search documentation

Arrow keys to move, Enter to open, Escape to close. Search stays in your browser.

Cookie settings