Developer reference
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.
https://license.refat.ovh/api/v1
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.
/activate. The server returns a unique
activation_token (64-char hex) for this site/device β returned once.wp_options, keychainβ¦). Never hardcode it./validate and /deactivate call includes an
HMAC-SHA256 signature generated with your stored token.
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,
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.
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
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.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.
100 per window10 minute(s), per identity and endpoint60 minute(s) once the limit is exceeded/activate additionally enforces a short per-license-key cooldown to stop reactivation spam.
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."
}
429, back off exponentially before retrying.
Don't validate on every page load β cache a successful result for 10β24 hours.
Read-only license status β no activation or signature required. Handy for a quick "is this key alive?" check.
license_keyrequiredhealth_tokenonly for license_key=health_check{
"status": "success",
"data": {
"status": "active",
"type": "Time Based",
"app_name": "My Awesome Software",
"expires_at": "December 31, 2026",
"remaining_days": 207
}
}
Links a license key to a specific domain or device and returns the activation_token you must
store and use to sign future requests.
license_keyrequiredapp_namerequired β must match the application your license is bound todomainweb β your site URLhardware_iddesktop/mobile β a unique device identifier (one of domain/hardware_id is required)client_metricsoptional β initial custom metrics as a JSON stringactivation_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"
}
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"
}
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.
license_keyrequiredapp_namerequireddomain / hardware_idrequired β one ofhmac_signaturerequired β see Security modeltimestamprequired β Unix seconds; β€300s past, β€60s futureapp_versionoptional β used for version lockingclient_metricsoptional β JSON string; ignored when a server-to-server backend is configuredincrement_usageoptional β integer to add to server-side usage counters{
"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.
Frees an activation slot for a domain/device. The stored activation_token is invalidated
server-side afterwards.
license_keyrequiredapp_namerequireddomain / hardware_idrequired β one ofhmac_signature + timestamprequired{
"status": "success",
"message": "License deactivated successfully."
}
Validate up to 50 license keys in a single request β ideal for server-side integrations that manage many licenses at once.
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"
}
]
}
{
"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." }
]
}
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.
| status | valid | Meaning |
|---|---|---|
| active | true | License is valid and active. |
| limit_reached | false | A usage limit was reached β message says which, and the relevant counts are included. |
| expired | false | License has expired (past the grace period). |
| suspended | false | License is suspended. |
| terminated | false | License is terminated / revoked. |
| inactive | false | No active activation found for this key + domain/hardware ID. |
| blocked | false | IP or country is blocked by license rules. |
| version | false | App version below the required minimum. |
| invalid | false | Unknown key, app-name mismatch, or missing required fields. |
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.
coderequiredamountoptional β order subtotal used to calculate the discount and final amountapp_idoptional β validates application scoping when suppliedemailoptional β used when the coupon has customer restrictions{
"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.
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" }
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.
license_keyrequiredapp_namerequiredhardware_id / domainrequired β the activated identity requesting the filehmac_signature + timestamprequired β same v2 signature as /validate; only an activated client holding its activation token can mint an offline file{
"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.
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" }
]
}
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.
/validate, the License Manager fetches live usage from your backend and returns it to the client.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}
HTTP/1.1 200 OK
Content-Type: application/json
{
"storage_used_gb": 45,
"users_created": 12,
"active_campaigns": 3,
"license_status": "active"
}
404 if the domain/device is unknown β the License
Manager safely falls back to its own local counters (usage_source reflects which was used).
// 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']); }
// 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.'); }
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'))
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 + "×tamp=" + 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(); } }
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.The /validate and /batch-validate responses may include these fields.
| Field | Type | When present | Description |
|---|---|---|---|
| usage_limit | int|null | Always | Total usage cap for usage-based licenses. null or 0 = unlimited. |
| hourly_limit | int|null | Always | Max metered units per hour. null = unlimited. |
| daily_limit / monthly_limit | int|null | Always | Usage caps. null = unlimited. |
| max_items | int|null | Always | Configured-items cap (e.g. seats, projects). |
| custom_limits | object|null | Always | Your app-specific thresholds, verbatim. |
| current_usage / hourly_usage / daily_usage / monthly_usage | int | Always | Current atomic counters after a successful increment. |
| last_hourly_reset / last_daily_reset / last_monthly_reset | datetime|null | Always | Period reset anchors. |
| usage_source | string | Always | backend or license_manager_local. |
| in_grace_period | bool | When valid & expired | Expired but still within the grace window. |
| warning | string | In grace period | Human-readable β show it to prompt renewal. |
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.
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."
}
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.
| Code | Meaning |
|---|---|
| 200 | Success. |
| 400 | Missing/invalid fields, app mismatch, malformed input, or activation-limit rejection. |
| 401 | Signature failed or timestamp outside the window (β€300s past, β€60s future). |
| 403 | Suspended / terminated / expired, blocked by an IP/country rule, or a usage limit was reached. |
| 404 | License key not found, or no active activation for this device/domain. |
| 429 | Rate limited or activation cooldown β back off and retry. |
| 500 | Server 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.