← Home

Developer reference

API Integration Guide

Everything you need to integrate your application with License Manager: activate, validate, and deactivate licenses, issue offline-signed license files, validate coupons, and meter usage. All endpoints are served under /api/v1, speak JSON, and return an X-Request-ID header for support and log correlation.

API base URL https://license.refat.ovh/api/v1

πŸ”’ Security model β€” per-activation token signing

Every device-bound /validate, /deactivate, offline-file, and untrusted batch item must be signed with an HMAC-SHA256 signature to prevent forgery and replay attacks. Each activation gets its own unique activation_token β€” no global secret is ever shared with clients.

How it works
  1. Your app calls /activate. The server returns a unique activation_token (64-char hex) for this site/device β€” returned once.
  2. Store the token securely (encrypted config, wp_options, keychain…). Never hardcode it.
  3. Every later /validate and /deactivate call includes an HMAC-SHA256 signature generated with your stored token.
  4. On deactivation the token is invalidated server-side; re-activation issues a fresh one.

Signature generation (scheme v2)

Build the canonical message β€” the literal tag v2 followed by each field length-prefixed as {byte_length}:{value}, in this exact order: license_key, app_name, domain_or_hardware_id, unix_timestamp β€” then sign it with your activation_token:

# Canonical message (length-prefixing removes delimiter ambiguity):
"v2" + "{len}:{license_key}" + "{len}:{app_name}" + "{len}:{identity}" + "{len}:{timestamp}"

# Example for key "LIC-1", app "Shop", domain "example.com", ts 1700000000:
v25:LIC-14:Shop11:example.com10:1700000000

# PHP
$timestamp = time();
$sign_data = 'v2';
foreach ([$license_key, $app_name, $domain, (string) $timestamp] as $field) {
    $sign_data .= strlen($field) . ':' . $field;
}
$signature = hash_hmac('sha256', $sign_data, $activation_token);

# Send in the POST body:
'hmac_signature' => $signature,
'timestamp'      => $timestamp,
Replay protection. The timestamp may be at most 300 seconds in the past and 60 seconds in the future (clock-skew allowance) relative to server time. Use a fresh timestamp for every request β€” a stale or reused one is rejected with 401.

Request IDs and support correlation

Every API response includes an X-Request-ID header. Send your own ID when you already have a trace or job identifier, or let the server generate a UUID. The accepted format is 8–128 characters using letters, numbers, dots, underscores, colons, or hyphens.

X-Request-ID: checkout-order-10492

# The same value is returned:
X-Request-ID: checkout-order-10492
When contacting support, include the response status, timestamp, endpoint, and X-Request-ID. The ID is attached to server log context so the exact request can be located without exposing a license key or activation token.

Rate limiting

To keep the service stable and fair, the API enforces a separate bucket per endpoint. Public/device calls are counted by trusted client IP; authenticated reseller calls are counted by API-token ID so multiple resellers behind the same proxy do not block one another.

Current default policy

  • Maximum requests: 100 per window
  • Window: 10 minute(s), per identity and endpoint
  • Block duration: 60 minute(s) once the limit is exceeded
  • Overrides: administrators may define license-key, IP, domain, or hardware-ID policies

/activate additionally enforces a short per-license-key cooldown to stop reactivation spam.

Exceeding the limit (HTTP 429)

HTTP/1.1 429 Too Many Requests
Retry-After: 3600
X-Request-ID: 7c803b1e-...

{
    "status": "error",
    "message": "Too many requests. Please slow down and try again later."
}
Best practices. On a 429, back off exponentially before retrying. Don't validate on every page load β€” cache a successful result for 10–24 hours.
POST/api/v1/status

Read-only license status β€” no activation or signature required. Handy for a quick "is this key alive?" check.

Parameters

  • license_keyrequired
  • health_tokenonly for license_key=health_check

Example response (200 OK)

{
    "status": "success",
    "data": {
        "status": "active",
        "type": "Time Based",
        "app_name": "My Awesome Software",
        "expires_at": "December 31, 2026",
        "remaining_days": 207
    }
}
POST/api/v1/activate

Links a license key to a specific domain or device and returns the activation_token you must store and use to sign future requests.

Parameters

  • license_keyrequired
  • app_namerequired β€” must match the application your license is bound to
  • domainweb β€” your site URL
  • hardware_iddesktop/mobile β€” a unique device identifier (one of domain/hardware_id is required)
  • client_metricsoptional β€” initial custom metrics as a JSON string

Success response (200 OK)

Store activation_token securely. You need it to sign every future /validate request. Do not hardcode it.
{
    "status": "success",
    "message": "License activated successfully.",
    "activation_token": "a3f8c2d1e4b5...64-char-hex"
}

Already active / reactivated

If the license is already active for this domain/device the existing token is returned so you can refresh your local copy (the request must then be signed with that token); reactivating a previously deactivated install issues a fresh token (the message differs).

{
    "status": "success",
    "message": "License already active for this device/domain.",
    "activation_token": "a3f8c2d1e4b5...64-char-hex"
}
POST/api/v1/validate

The endpoint you call regularly to confirm a license is still valid for the current domain/device, meter usage, and read live limits. Must be signed with your stored activation_token.

Parameters

  • license_keyrequired
  • app_namerequired
  • domain / hardware_idrequired β€” one of
  • hmac_signaturerequired β€” see Security model
  • timestamprequired β€” Unix seconds; ≀300s past, ≀60s future
  • app_versionoptional β€” used for version locking
  • client_metricsoptional β€” JSON string; ignored when a server-to-server backend is configured
  • increment_usageoptional β€” integer to add to server-side usage counters

Success response (200 OK)

{
    "valid": true,
    "status": "active",
    "license_type": "time_based",
    "license_level": "premium",
    "expires_at": "2026-07-31 00:00:00",
    "current_usage": 15,
    "usage_limit": 50000,
    "daily_limit": 1000,
    "hourly_limit": 100,
    "monthly_limit": 20000,
    "max_items": 50,
    "custom_limits": { "active_projects": 5, "storage_gb": 50 },
    "hourly_usage": 18,
    "daily_usage": 142,
    "monthly_usage": 3800,
    "last_hourly_reset": "2026-06-07 09:00:00",
    "last_daily_reset": "2026-06-07 00:00:00",
    "last_monthly_reset": "2026-06-01",
    "usage_source": "license_manager_local",

    // Only present while in the post-expiry grace window:
    "in_grace_period": true,
    "warning": "License expired but is within the grace period."
}

A null limit means unlimited. custom_limits is your app-specific threshold JSON, passed through verbatim so your client can enforce its own rules.

POST/api/v1/deactivate

Frees an activation slot for a domain/device. The stored activation_token is invalidated server-side afterwards.

Parameters

  • license_keyrequired
  • app_namerequired
  • domain / hardware_idrequired β€” one of
  • hmac_signature + timestamprequired

Success response (200 OK)

{
    "status": "success",
    "message": "License deactivated successfully."
}
POST/api/v1/batch-validate

Validate up to 50 license keys in a single request β€” ideal for server-side integrations that manage many licenses at once.

Request body (JSON)

POST https://license.refat.ovh/api/v1/batch-validate
Content-Type: application/json

{
    "licenses": [
        {
            "license_key": "LIC-XXXX-0001", "app_name": "MyApp", "domain": "site1.com",
            "timestamp": 1780819200, "hmac_signature": "per-activation-signature"
        },
        {
            "license_key": "LIC-XXXX-0002", "app_name": "MyApp", "hardware_id": "HW-ABC-123",
            "timestamp": 1780819200, "hmac_signature": "per-activation-signature"
        }
    ]
}

Example response (200 OK)

{
    "status": "success",
    "results": [
        { "license_key": "LIC-XXXX-0001", "valid": true,  "status": "active",
          "license_level": "premium", "expires_at": "2026-12-31 00:00:00",
          "daily_limit": 1000, "daily_usage": 142 },
        { "license_key": "LIC-XXXX-0002", "valid": false, "status": "limit_reached",
          "message": "Monthly usage limit reached.",
          "monthly_usage": 20000, "monthly_limit": 20000 },
        { "license_key": "LIC-XXXX-0003", "valid": false, "status": "expired",
          "message": "This license has expired." }
    ]
}
Max 50 items per request. All standard checks (IP/country rules, expiry, usage limits) apply to each key. Each untrusted item must include its own current timestamp and HMAC signature, because each activation has a different token. A trusted backend may instead send the application-level X-Backend-Token header. Keep batch validation server-side.

Result status values per key

statusvalidMeaning
activetrueLicense is valid and active.
limit_reachedfalseA usage limit was reached β€” message says which, and the relevant counts are included.
expiredfalseLicense has expired (past the grace period).
suspendedfalseLicense is suspended.
terminatedfalseLicense is terminated / revoked.
inactivefalseNo active activation found for this key + domain/hardware ID.
blockedfalseIP or country is blocked by license rules.
versionfalseApp version below the required minimum.
invalidfalseUnknown key, app-name mismatch, or missing required fields.
POST/api/v1/coupon/validate

Validate a coupon before applying it in your storefront or checkout. This endpoint only checks eligibility; the reseller/license-creation action performs the authoritative redemption when the license is issued.

Parameters

  • coderequired
  • amountoptional β€” order subtotal used to calculate the discount and final amount
  • app_idoptional β€” validates application scoping when supplied
  • emailoptional β€” used when the coupon has customer restrictions

Example response

{
    "valid": true,
    "discount_amount": 25,
    "final_amount": 75,
    "coupon": {
        "code": "LAUNCH25",
        "description": "Launch discount",
        "discount_type": "percentage",
        "discount_value": 25,
        "currency": "USD"
    }
}

An ineligible or unknown code normally returns HTTP 200 with {"valid":false,"reason":"…"}; a missing code is a malformed request and returns HTTP 400.

Do not grant a license based only on this pre-check. Send the coupon code again with the reseller create/upsert request so redemption limits are enforced transactionally.
GET/api/v1/health-ping

Liveness probe β€” confirms the API and database are reachable. Use it with your uptime monitor.

{ "status": "success", "message": "API is healthy.", "time": "2026-06-07T00:00:00+00:00" }

Offline licensing (Ed25519)

For air-gapped or intermittently-connected environments, request a cryptographically signed license file your app can verify locally β€” no network call needed until it re-checks in.

POST/api/v1/license-file

Parameters

  • license_keyrequired
  • app_namerequired
  • hardware_id / domainrequired β€” the activated identity requesting the file
  • hmac_signature + timestamprequired β€” same v2 signature as /validate; only an activated client holding its activation token can mint an offline file

Success response (200 OK)

{
    "status": "success",
    "license_file": "<base64url envelope>",
    "kid": "k_2026_06",
    "expires_at": 1781568000,
    "ttl_seconds": 604800
}

The file's TTL is min(license expiry βˆ’ now, configured offline TTL) β€” default 7 days, adjustable in Settings β†’ API. Because an offline file keeps validating until it expires, the TTL is the de-facto revocation window for suspended/revoked licenses. The envelope is base64url(JSON({header, payload, signature})), signed with Ed25519 over the canonical header and payload.

GET/api/v1/public-keys

Published verification keys. Pin these in your app to verify offline files; secret keys are never exposed.

{
    "status": "success",
    "keys": [
        { "kid": "k_2026_06", "public_key": "<base64>", "algorithm": "Ed25519",
          "is_active": true, "created_at": "2026-06-01T00:00:00Z" }
    ]
}

Server-to-server (anti-null) integration

For maximum protection, the License Manager can pull live usage directly from your application backend on each /validate. A modified (nulled) client then cannot fake low usage numbers, because the real counts live on your server.

How it works.
  1. When a license is activated, a webhook can broadcast its limits to your backend.
  2. Your backend tracks and increments usage server-side as the software runs.
  3. On /validate, the License Manager fetches live usage from your backend and returns it to the client.

The backend contract

If enabled, the License Manager issues this request to your backend whenever a client validates:

GET {Your_Backend_URL}/api/internal/license-usage/{domain_or_hardware_id}
Accept: application/json
X-Backend-Token: {Your_Secure_Auth_Token}

Expected JSON response

HTTP/1.1 200 OK
Content-Type: application/json

{
    "storage_used_gb": 45,
    "users_created": 12,
    "active_campaigns": 3,
    "license_status": "active"
}
Fallback. Return HTTP 404 if the domain/device is unknown β€” the License Manager safely falls back to its own local counters (usage_source reflects which was used).

Integration examples

Step 1 β€” Activation (run once, store the token)

// PHP
$response = file_get_contents('https://license.refat.ovh/api/v1/activate', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'license_key' => 'YOUR_LICENSE_KEY',
            'app_name'    => 'YOUR_APP_NAME',
            'domain'      => 'example.com',
        ]),
    ],
]));

$result = json_decode($response, true);
if (($result['status'] ?? '') === 'success') {
    // Persist securely β€” never hardcode
    update_option('my_activation_token', $result['activation_token']);
}

Step 2 β€” Validation (signed, run on a schedule)

// PHP
$license_key      = 'YOUR_LICENSE_KEY';
$app_name         = 'YOUR_APP_NAME';
$domain           = 'example.com';
$activation_token = get_option('my_activation_token');
$timestamp        = time();

// Canonical v2 message: 'v2' + length-prefixed fields (see Security model)
$sign_data = 'v2';
foreach ([$license_key, $app_name, $domain, (string) $timestamp] as $field) {
    $sign_data .= strlen($field) . ':' . $field;
}
$signature = hash_hmac('sha256', $sign_data, $activation_token);

$result = json_decode(file_get_contents('https://license.refat.ovh/api/v1/validate', false, stream_context_create([
    'http' => [
        'method'  => 'POST',
        'header'  => 'Content-Type: application/x-www-form-urlencoded',
        'content' => http_build_query([
            'license_key'     => $license_key,
            'app_name'        => $app_name,
            'domain'          => $domain,
            'app_version'     => '1.0.0',
            'hmac_signature'  => $signature,
            'timestamp'       => $timestamp,
            'increment_usage' => 1,
        ]),
    ],
])), true);

if (($result['valid'] ?? false) === true) {
    if (!empty($result['in_grace_period'])) {
        // Expired but still working β€” prompt renewal
        notice($result['warning']);
    }
    run_application();
} else {
    block_application($result['message'] ?? 'License invalid.');
}

Python (requests)

import requests, hmac, hashlib, time

API   = 'https://license.refat.ovh/api/v1'
KEY   = 'YOUR_LICENSE_KEY'
APP   = 'YOUR_APP_NAME'
HWID  = 'UNIQUE_MACHINE_ID'

# Activation (run once)
token = requests.post(f'{API}/activate', data={
    'license_key': KEY, 'app_name': APP, 'hardware_id': HWID,
}).json()['activation_token']
# persist `token` securely

# Validation (run on a schedule) β€” canonical v2 message (length-prefixed)
ts   = int(time.time())
msg  = 'v2' + ''.join(f'{len(f.encode())}:{f}' for f in (KEY, APP, HWID, str(ts)))
sig  = hmac.new(token.encode(), msg.encode(), hashlib.sha256).hexdigest()
res  = requests.post(f'{API}/validate', data={
    'license_key': KEY, 'app_name': APP, 'hardware_id': HWID,
    'hmac_signature': sig, 'timestamp': ts, 'increment_usage': 1,
}).json()

print('valid' if res.get('valid') else res.get('message'))

Android (Java β€” HttpURLConnection)

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.json.JSONObject; // add a JSON library such as org.json

public class LicenseValidator {

    private static final String API_URL = "https://license.refat.ovh/api/v1/validate";

    private static String hmacSha256(String data, String key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        StringBuilder hex = new StringBuilder();
        for (byte b : mac.doFinal(data.getBytes(StandardCharsets.UTF_8))) {
            hex.append(String.format("%02x", b));
        }
        return hex.toString();
    }

    public static void validateLicense(String licenseKey, String appName, String identifier,
                                       boolean isHardwareId, String activationToken) {
        new Thread(() -> {
            HttpURLConnection connection = null;
            try {
                long timestamp = System.currentTimeMillis() / 1000L;
                // Canonical v2 message: "v2" + byte-length-prefixed fields
                StringBuilder signData = new StringBuilder("v2");
                for (String field : new String[] { licenseKey, appName, identifier, String.valueOf(timestamp) }) {
                    signData.append(field.getBytes(StandardCharsets.UTF_8).length).append(':').append(field);
                }
                String signature = hmacSha256(signData.toString(), activationToken); // token stored at activation

                String identifierKey = isHardwareId ? "hardware_id" : "domain";
                String urlParameters = "license_key=" + URLEncoder.encode(licenseKey, "UTF-8") +
                        "&app_name=" + URLEncoder.encode(appName, "UTF-8") +
                        "&" + identifierKey + "=" + URLEncoder.encode(identifier, "UTF-8") +
                        "&increment_usage=1" +
                        "&hmac_signature=" + signature +
                        "&timestamp=" + timestamp;

                URL url = new URL(API_URL);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                connection.setDoOutput(true);
                try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
                    wr.write(urlParameters.getBytes(StandardCharsets.UTF_8));
                }

                try (BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
                    StringBuilder response = new StringBuilder();
                    String line;
                    while ((line = in.readLine()) != null) response.append(line);

                    JSONObject json = new JSONObject(response.toString());
                    if (json.optBoolean("valid")) {
                        Object dailyLimit = json.opt("daily_limit"); // null = unlimited
                        String limit = (dailyLimit instanceof Integer) ? String.valueOf(dailyLimit) : "Unlimited";
                        System.out.println("Daily usage: " + json.optInt("daily_usage", 0) + " / " + limit);
                    } else {
                        System.out.println("License invalid: " + json.optString("message", "Unknown error"));
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (connection != null) connection.disconnect();
            }
        }).start();
    }
}
For desktop/mobile apps use a stable hardware_id; for web apps use domain. Run the activation once, store the returned activation_token securely (Keystore / encrypted preferences), and sign every later /validate call with it as shown above.

Response fields & behaviors

The /validate and /batch-validate responses may include these fields.

FieldTypeWhen presentDescription
usage_limitint|nullAlwaysTotal usage cap for usage-based licenses. null or 0 = unlimited.
hourly_limitint|nullAlwaysMax metered units per hour. null = unlimited.
daily_limit / monthly_limitint|nullAlwaysUsage caps. null = unlimited.
max_itemsint|nullAlwaysConfigured-items cap (e.g. seats, projects).
custom_limitsobject|nullAlwaysYour app-specific thresholds, verbatim.
current_usage / hourly_usage / daily_usage / monthly_usageintAlwaysCurrent atomic counters after a successful increment.
last_hourly_reset / last_daily_reset / last_monthly_resetdatetime|nullAlwaysPeriod reset anchors.
usage_sourcestringAlwaysbackend or license_manager_local.
in_grace_periodboolWhen valid & expiredExpired but still within the grace window.
warningstringIn grace periodHuman-readable β€” show it to prompt renewal.

Usage / hourly limit reached

When a usage limit is exhausted, validation fails closed with HTTP 403 and status: limit_reached. The license record may still be active, but the requested entitlement is unavailable until the period resets or an administrator raises the cap. Back off or queue:

{
    "valid": false,
    "status": "limit_reached",
    "message": "Hourly usage limit reached.",
    "hourly_usage": 100,
    "hourly_limit": 100
}

Daily, monthly, and total-usage rejections share the same shape with their own counters and limits. Batch validation returns these diagnostic fields on the affected result item as well.

Handling the grace period in your integration

While a license is expired but inside its grace window, /validate still returns valid: true with in_grace_period: true β€” keep the app running and show the warning to prompt renewal:

# Python example
result = requests.post(API + '/validate', data={...}).json()

if result.get('valid'):
    if result.get('in_grace_period'):
        # Expired but still works β€” show a renewal warning
        show_warning(result.get('warning', 'Your license has expired. Please renew.'))
    run_application()
else:
    block_application(result.get('message', 'License invalid.'))

Once the grace period ends, validation fails with HTTP 403:

{
    "valid": false,
    "status": "expired",
    "message": "This license has expired."
}

Read-only reseller verification for protected downloads

If another system such as Updator needs to decide whether a customer may download a licensed update, use the reseller / billing-sync endpoint with an expiring read-only API token and the verify_license action. This keeps public/free projects independent, while licensed projects can make one clean server-side check for license ownership, status, expiry, and matching app_id.

POST https://license.refat.ovh/api/v1/reseller
Authorization: Bearer READ_ONLY_TOKEN
Content-Type: application/json

{
  "action": "verify_license",
  "license_key": "LIC-EXAMPLE",
  "app_id": 1,
  "purpose": "updator_download"
}

Successful response shape: {status, valid, reason, message, license}. Invalid licenses return valid: false with reasons such as license_not_found, app_mismatch, status_suspended, or expired. Every attempt is logged in the admin Audit Log with the result, reason, purpose, IP, user agent, app ID, and license key. Rotate tokens before expiry by creating and testing a replacement, deploying it, and only then revoking the old token.

Status-code reference

CodeMeaning
200Success.
400Missing/invalid fields, app mismatch, malformed input, or activation-limit rejection.
401Signature failed or timestamp outside the window (≀300s past, ≀60s future).
403Suspended / terminated / expired, blocked by an IP/country rule, or a usage limit was reached.
404License key not found, or no active activation for this device/domain.
429Rate limited or activation cooldown β€” back off and retry.
500Server error. Record the X-Request-ID before retrying or contacting support.

Need to manage licenses programmatically or verify licenses from another server? That's the reseller / billing-sync API. Sign in to your reseller panel and open API Tokens to generate a read-only token for verification/download gates or a full-access token for billing sync. Don't have reseller access yet? Ask your account administrator.