License Authorization
The authorization endpoint is the core of GeckoGuard — it validates a license key and enforces your product's policy (HWID binding, IP restrictions, concurrency limits, blacklists, expiration).
Endpoint
POST /v1/licenses/authorize
Authentication: API Key with license:authorize permission plus a signed request.
This endpoint is the brute-force target for stolen keys, so it requires HMAC-SHA256 signing in addition to the API key. Three extra headers must be sent: X-GG-Timestamp, X-GG-Nonce, X-GG-Signature. Unsigned requests are rejected with 401 SIGNATURE_REQUIRED.
The official SDKs sign requests for you. If you're calling REST directly, follow the signing recipe in the Authentication guide — it includes a Node.js reference implementation.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
productId | string (UUID) | Yes | The product this license belongs to |
licenseKey | string | Yes | The license key to validate |
hwid | string | No | Hardware identifier for device binding |
ip | string | No | Client IP for IP-based restrictions |
deviceId | string | No | Unique device identifier |
sessionId | string | No | Session identifier for concurrency limits |
dryRun | boolean | No | If true, evaluates the policy without persisting changes |
Responses
Authorized (200)
{
"ok": true,
"allow": true,
"reasonCode": "ALLOW",
"message": "Authorization granted",
"policy": {
"summary": "Authorization granted"
},
"limits": {
"remaining": {
"hwid": "unlimited",
"ip": "unlimited",
"concurrency": 3
}
},
"activatedAt": "2025-01-15T10:30:00.000Z",
"effectiveExpiresAt": "2025-12-31T23:59:59.000Z",
"expiresAt": "2025-12-31T23:59:59.000Z",
"expiresAfterDays": null
}
Denied (403)
{
"ok": false,
"allow": false,
"reasonCode": "HWID_MISMATCH",
"message": "Hardware ID does not match bound device"
}
Not Found (404)
{
"ok": false,
"error": "LICENSE_NOT_FOUND",
"message": "License key not found"
}
Reason Codes
| Code | Meaning |
|---|---|
ALLOW | Authorization granted |
LICENSE_NOT_FOUND | Key does not exist |
PRODUCT_MISMATCH | Key belongs to a different product |
LICENSE_REVOKED | License is revoked or frozen |
LICENSE_EXPIRED | Fixed expiration date has passed |
LICENSE_EXPIRED_RELATIVE | Expired based on days-after-activation |
HWID_MISMATCH | Device doesn't match bound HWID (sticky mode) |
HWID_LIMIT_EXCEEDED | Too many distinct devices (limit mode) |
IP_MISMATCH | IP doesn't match bound address |
IP_LIMIT_EXCEEDED | Too many distinct IPs |
IP_REGION_MISMATCH | IP region not in allowed list |
CONCURRENCY_LIMIT_EXCEEDED | Too many active sessions |
HWID_BLACKLISTED | Hardware ID is blacklisted |
IP_BLACKLISTED | IP address is blacklisted |
Side Effects
A successful authorization may:
- Set
activatedAton first call (for licenses withexpiresAfterDays) - Create or update bindings (HWID, IP records)
- Create or update sessions (for concurrency tracking)
- Fire
license.authorizedwebhook - Send "new device detected" email to the product owner
Use dryRun: true to test without any side effects.
Code Examples
JavaScript / Node.js
const GECKOGUARD_API = 'https://api.geckoguard.net';
const API_KEY = process.env.GECKOGUARD_API_KEY;
async function validateLicense(licenseKey, hwid) {
const res = await fetch(`${GECKOGUARD_API}/v1/licenses/authorize`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
productId: 'YOUR_PRODUCT_ID',
licenseKey,
hwid,
}),
});
const data = await res.json();
if (data.allow) {
console.log('License valid. Expires:', data.effectiveExpiresAt);
return true;
}
console.error('Denied:', data.reasonCode, data.message);
return false;
}
Python
import requests
import os
API_KEY = os.environ["GECKOGUARD_API_KEY"]
PRODUCT_ID = "YOUR_PRODUCT_ID"
def validate_license(license_key: str, hwid: str = None) -> dict:
resp = requests.post(
"https://api.geckoguard.net/v1/licenses/authorize",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"productId": PRODUCT_ID,
"licenseKey": license_key,
"hwid": hwid,
},
)
data = resp.json()
if data.get("allow"):
return {"valid": True, "expires_at": data.get("effectiveExpiresAt")}
return {"valid": False, "reason": data.get("reasonCode"), "message": data.get("message")}
C# / .NET
using System.Net.Http;
using System.Net.Http.Json;
public class GeckoGuardClient
{
private readonly HttpClient _http;
private readonly string _productId;
public GeckoGuardClient(string apiKey, string productId)
{
_productId = productId;
_http = new HttpClient();
_http.BaseAddress = new Uri("https://api.geckoguard.net/");
_http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task<AuthResult> ValidateAsync(string licenseKey, string hwid = null)
{
var response = await _http.PostAsJsonAsync("v1/licenses/authorize", new
{
productId = _productId,
licenseKey,
hwid,
});
var data = await response.Content.ReadFromJsonAsync<AuthResponse>();
return new AuthResult
{
Valid = data.Allow,
ReasonCode = data.ReasonCode,
Message = data.Message,
ExpiresAt = data.EffectiveExpiresAt,
};
}
}
C++
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <string>
using json = nlohmann::json;
struct AuthResult {
bool valid;
std::string reasonCode;
std::string message;
std::string expiresAt;
};
AuthResult validateLicense(
const std::string& apiKey,
const std::string& productId,
const std::string& licenseKey,
const std::string& hwid = "")
{
json body = {
{"productId", productId},
{"licenseKey", licenseKey}
};
if (!hwid.empty()) body["hwid"] = hwid;
CURL* curl = curl_easy_init();
std::string response;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, ("Authorization: Bearer " + apiKey).c_str());
headers = curl_slist_append(headers, "Content-Type: application/json");
std::string payload = body.dump();
curl_easy_setopt(curl, CURLOPT_URL, "https://api.geckoguard.net/v1/licenses/authorize");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
+[](char* ptr, size_t size, size_t nmemb, std::string* data) -> size_t {
data->append(ptr, size * nmemb);
return size * nmemb;
});
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
auto data = json::parse(response);
return {
data.value("allow", false),
data.value("reasonCode", ""),
data.value("message", ""),
data.value("effectiveExpiresAt", "")
};
}
Lua
local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("cjson")
local API_KEY = "YOUR_API_KEY"
local PRODUCT_ID = "YOUR_PRODUCT_ID"
function validateLicense(licenseKey, hwid)
local body = json.encode({
productId = PRODUCT_ID,
licenseKey = licenseKey,
hwid = hwid
})
local response = {}
http.request({
url = "https://api.geckoguard.net/v1/licenses/authorize",
method = "POST",
headers = {
["Authorization"] = "Bearer " .. API_KEY,
["Content-Type"] = "application/json",
["Content-Length"] = #body
},
source = ltn12.source.string(body),
sink = ltn12.sink.table(response)
})
local data = json.decode(table.concat(response))
return data.allow == true, data.reasonCode, data.message
end
Best Practices
- Always send HWID — even if your policy is set to unlimited, binding data is recorded for analytics and future policy changes.
- Use
sessionIdfor concurrency — pass a unique session ID per running instance. Sessions expire automatically when inactive. - Cache results — don't call authorize on every frame. Check once at startup and periodically (e.g., every 5–15 minutes).
- Handle denials gracefully — show the user a specific message based on
reasonCoderather than a generic error. - Use
dryRunfor testing — validate your integration without creating real bindings.