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

FieldTypeRequiredDescription
productIdstring (UUID)YesThe product this license belongs to
licenseKeystringYesThe license key to validate
hwidstringNoHardware identifier for device binding
ipstringNoClient IP for IP-based restrictions
deviceIdstringNoUnique device identifier
sessionIdstringNoSession identifier for concurrency limits
dryRunbooleanNoIf 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

CodeMeaning
ALLOWAuthorization granted
LICENSE_NOT_FOUNDKey does not exist
PRODUCT_MISMATCHKey belongs to a different product
LICENSE_REVOKEDLicense is revoked or frozen
LICENSE_EXPIREDFixed expiration date has passed
LICENSE_EXPIRED_RELATIVEExpired based on days-after-activation
HWID_MISMATCHDevice doesn't match bound HWID (sticky mode)
HWID_LIMIT_EXCEEDEDToo many distinct devices (limit mode)
IP_MISMATCHIP doesn't match bound address
IP_LIMIT_EXCEEDEDToo many distinct IPs
IP_REGION_MISMATCHIP region not in allowed list
CONCURRENCY_LIMIT_EXCEEDEDToo many active sessions
HWID_BLACKLISTEDHardware ID is blacklisted
IP_BLACKLISTEDIP address is blacklisted

Side Effects

A successful authorization may:

  • Set activatedAt on first call (for licenses with expiresAfterDays)
  • Create or update bindings (HWID, IP records)
  • Create or update sessions (for concurrency tracking)
  • Fire license.authorized webhook
  • 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

  1. Always send HWID — even if your policy is set to unlimited, binding data is recorded for analytics and future policy changes.
  2. Use sessionId for concurrency — pass a unique session ID per running instance. Sessions expire automatically when inactive.
  3. Cache results — don't call authorize on every frame. Check once at startup and periodically (e.g., every 5–15 minutes).
  4. Handle denials gracefully — show the user a specific message based on reasonCode rather than a generic error.
  5. Use dryRun for testing — validate your integration without creating real bindings.