AstraGuard - Now Available

License management
without the headache.

Ship licenses, validate clients, and monitor usage with HWID binding, real-time fraud detection, and a developer-first API - all from one dashboard.

Open Dashboard →
<200ms
Validation latency
99.9%
Uptime SLA
Edge
Cloudflare CDN
HMAC
Response signing

What is AstraGuard?

AstraGuard is a SaaS license management & software protection platform built for developers - especially game tool and loader developers. It handles the hard parts: key generation, hardware binding, fraud detection, and secure file distribution.

License Keys

Perpetual, subscription, and trial keys. Bulk generation, CSV export, batch operations.

HWID Binding

Lock licenses to hardware. Customers request resets through the portal.

Feature Flags

Toggle features remotely - returned on every validation. No redeploy needed.

Analytics

Real-time activations, fraud alerts, revenue, and user growth dashboards.

Webhooks

Event-driven notifications for activations, revocations, HWID resets, and fraud.

File Distribution

Upload builds to R2. Customers download securely through the portal.

User Roles

RoleAccess
developerProducts, keys, files, analytics, resellers
resellerReseller dashboard - manage quotas and customer keys
customerCustomer portal - license info, HWID reset, downloads

How AstraGuard Protects Licenses

Protection comes from several independent layers working together - no single one is meant to carry the whole job on its own:

  • Server-side validation - your software calls /validate on every launch. License state lives on AstraGuard's servers; it's never something the client alone gets to decide.
  • HWID binding - a key activates on one machine and stays locked there until the customer requests (and you approve) a reset.
  • Response signing - every response is cryptographically signed so a local proxy or MITM can't quietly swap a rejection for an approval. See the Response Key page.
  • Optional runtime checks - per-product anti-debug and anti-VM flags, enforced server-side at validation time.

See the Hardening Guide for the full production checklist, and Security Best Practices for broader integration patterns.

Recommended Path

  1. Installation - Install the SDK
  2. Quickstart - Create product → generate key → validate in 5 min
  3. Validation - Integrate POST /validate in your software
  4. Response Key - Verify server responses to prevent MITM
  5. Webhooks - Get notified on key events

Installation

Install the AstraGuard TypeScript/JavaScript SDK or use the C++ header for native applications.

Requirements

  • Node.js 16.x or higher
  • npm 7+, yarn 1.22+, or pnpm 8+
  • TypeScript 4.5+ (optional but recommended)

Package Installation

bash
npm install @astraguard/sdk
# or
yarn add @astraguard/sdk
# or
pnpm add @astraguard/sdk

Environment Variables

bash
ASTRAGUARD_API_URL=https://api.astraguard.io
ASTRAGUARD_PRODUCT_ID=your-product-uuid

Never commit secrets

Add .env to your .gitignore. Your product ID is not a secret, but never expose backend API keys in client-side code.

C++ Integration

Download AstraGuard.h from Products → Integration in your dashboard. Drop it into your project - no dependencies required.

XOR Obfuscation

The C++ header XOR-obfuscates your product ID using a compile-time XOR key embedded in the header to prevent extraction via the strings tool. Deobfuscation happens at runtime automatically.

Quickstart

From zero to validating your first license key in under 5 minutes.

Step 1 - Create a Product

Log in → Products → New Product. Give it a name and optional key prefix (e.g. VIZ- produces keys like VIZ-XXXX-XXXX-XXXX).

Step 2 - Generate a Key

Inside your product click Generate Keys. Select type perpetual, count 1, click Generate. Copy the key.

Step 3 - Install the SDK

bash
npm install @astraguard/sdk

Step 4 - Validate

typescript
import { createClient } from '@astraguard/sdk'

const guard = createClient({
  apiUrl:    'https://api.astraguard.io',
  productId: process.env.ASTRAGUARD_PRODUCT_ID,
})

const result = await guard.validate('XXXX-XXXX-XXXX-XXXX')

if (result.valid) {
  console.log('License valid - expires:', result.expiresAt)
  console.log('Features:', result.features)
} else {
  console.log('Invalid:', result.reason)
}

Expected Response

json
{
  "valid": true,
  "expiresAt": null,
  "features": [{ "name": "pro", "enabled": true }],
  "variables": { "serverUrl": "https://prod.example.com" }
}

You're ready!

Next: add HWID binding to lock keys to hardware, set up Feature Flags, or configure Webhooks for event notifications.

Products

A Product is the software you want to protect. Each product has its own keys, feature flags, variables, webhooks, and security settings.

Creating a Product

Go to Products → New Product and configure:

FieldDescription
nameDisplay name
keyPrefixOptional prefix - e.g. VIZ- → keys look like VIZ-XXXX-XXXX
imageUrlProduct icon (HTTPS, max 500 chars)
minVersionMinimum client version - older versions are rejected
webhookUrlURL to POST events to

Security Settings

SettingEffect
blockVmReject activations from virtual machines
blockDebugReject if a debugger is attached at activation time
integrityCheckVerify binary hash matches stored integrityHash
quantumSafeUse quantum-resistant signature algorithm for responses

More product-level restrictions

You can also restrict activations to specific countries via Geo-Blocking, or bundle multiple products together so one key unlocks all of them. See Geo-Blocking & IP Whitelist and Bundle Keys in the sidebar.

Version Enforcement

Set minVersion on the product. When a validate request includes a version field below minVersion:

json
{ "valid": false, "reason": "version_too_old" }

Your software should show an "update required" dialog and exit.

Plan Limits

PlanProductsLicensesFiles/ProductReleases/ProductAPI Calls/DayHWID Resets/Month
Starter1100111,0003
MaxUnlimitedUnlimitedUnlimitedUnlimitedUnlimitedUnlimited

Starter plan - free forever

The Starter plan has no time limit. Features like webhooks, fraud detection, HWID blacklisting, Quantum-Safe signing, and Flexible HWID mode require the Max plan ($4.99/month).

License Keys

License keys are the tokens your customers use to activate and validate your software. Perpetual, subscription, and trial key types are supported.

Key Types

TypeExpiryUse Case
perpetualNeverOne-time purchase, lifetime access
subscriptionConfigurable (days)Recurring monthly/yearly billing
trial1-720 hoursFree trial, demo access

Key Lifecycle

text
unused  →  active (first activation binds HWID)
                ↓
         expired / revoked / banned / frozen

Key States

StateDescriptionReversible
activeIn use, HWID bound-
unusedGenerated but never activated-
frozenTemporarily disabled by developerYes - unfreeze
revokedDeactivated by developerYes - unrevoke
bannedBanned for abusePermanent
expiredPast expires_at dateExtend expiry

HWID Binding

On first activation the key binds to the HWID sent by the client. All subsequent /validate calls must send the same HWID. A mismatch returns { "valid": false, "reason": "hwid_mismatch" }.

Per-key IP restriction

You can lock an individual key to one or more trusted IP addresses. Useful for server deployments where the client IP is fixed. See Geo-Blocking & IP Whitelist in the sidebar.

Bulk Operations

Select multiple keys in the key table to bulk revoke, freeze, unfreeze, or delete. Export all keys as CSV via Export button.

Bundle Keys

Bundle multiple products together so that one license key unlocks access to all of them. When a key for the primary product is validated, the response automatically includes the list of bundled products - no extra API calls needed in your software.

Max plan required

Bundle Keys is available on the Max plan only. Attempting to add a bundle on the Starter plan returns ERR_PLAN_LIMIT.

How It Works

You nominate one product as the primary and attach one or more other products as included bundles. When a customer validates a key for the primary product, the /validate response includes a bundledAccess array listing every bundled product. Your software reads this array to decide which features or modules to unlock.

text
Primary product: "Pro Suite"
  └─ Bundled: "Module A"
  └─ Bundled: "Module B"

Customer validates key for "Pro Suite"
  → { valid: true, bundledAccess: [
       { id: "uuid-a", name: "Module A" },
       { id: "uuid-b", name: "Module B" }
     ] }

Your software checks bundledAccess to unlock Module A and Module B

Configuring Bundles

Bundles are managed in Product Settings → Bundle Keys in the dashboard, or via the API.

Add a bundle

http
POST /products/:primaryProductId/bundles
Authorization: Bearer <token>
Content-Type: application/json

{
  "includedProductId": "uuid-of-product-to-bundle"
}

List bundles

http
GET /products/:primaryProductId/bundles
Authorization: Bearer <token>

// Response
[
  { "id": "uuid-a", "name": "Module A" },
  { "id": "uuid-b", "name": "Module B" }
]

Remove a bundle

http
DELETE /products/:primaryProductId/bundles/:includedProductId
Authorization: Bearer <token>

Validate Response with Bundle Access

A successful /validate call for a bundled primary product includes bundledAccess in the response:

json
{
  "valid": true,
  "expiresAt": "2027-01-01T00:00:00.000Z",
  "features": [...],
  "variables": {},
  "bundledAccess": [
    { "id": "uuid-a", "name": "Module A" },
    { "id": "uuid-b", "name": "Module B" }
  ]
}

If no bundles are configured, bundledAccess is an empty array [].

Reading Bundle Access in Your Software

typescript
const result = await guard.validate(licenseKey)
if (!result.valid) return

const BUNDLE_MODULE_A = 'uuid-a'
const BUNDLE_MODULE_B = 'uuid-b'

const hasModuleA = result.bundledAccess?.some(p => p.id === BUNDLE_MODULE_A)
const hasModuleB = result.bundledAccess?.some(p => p.id === BUNDLE_MODULE_B)

if (hasModuleA) enableModuleA()
if (hasModuleB) enableModuleB()
cpp
// C++ - parse bundledAccess from validate JSON response
// The AstraGuard.h validate() call returns a JSON string
// Parse it and check for bundled product IDs

auto response = json::parse(validateResult);
auto& bundled = response["bundledAccess"];
bool hasModuleA = false;
for (auto& p : bundled) {
    if (p["id"] == "uuid-a") hasModuleA = true;
}
if (hasModuleA) EnableModuleA();

Constraints

  • You can only bundle products you own - cross-developer bundling is not allowed
  • A product cannot be bundled with itself
  • There is no limit on how many products can be included in a bundle
  • Removing a bundle takes effect immediately on the next /validate call
  • Bundle access is read-only in the validate response - keys for the bundled products themselves are not created or affected

Features & Variables

Feature flags gate functionality in your software remotely. Remote variables push config values without redeploying.

Feature Flags

Go to Products → [Product] → Features. Each flag has a name and an enabled toggle. They are returned in every /validate response:

json
{
  "valid": true,
  "features": [
    { "name": "pro_mode",   "enabled": true  },
    { "name": "beta_tools", "enabled": false }
  ]
}

Remote Variables

Go to Products → [Product] → Variables. Key-value pairs returned in /validate:

json
{
  "valid": true,
  "variables": {
    "serverUrl":  "https://prod.example.com",
    "maxRetries": "3",
    "debugMode":  "false"
  }
}

Secret Variables

Mark a variable isSecret: true - it will never be sent to the client. Use for sensitive backend configuration that shouldn't be in client memory.

Values are always strings

Parse numbers/booleans in your client: parseInt(vars.maxRetries), vars.debugMode === 'true'.

Checking Feature Flags in Client Code

A small helper keeps your gating code clean:

typescript
const result = await guard.validate('XXXX-XXXX-XXXX-XXXX')

function hasFeature(name: string): boolean {
  return result.features?.some(f => f.name === name && f.enabled) ?? false
}

if (hasFeature('pro_mode')) {
  enableProTools()
}
if (hasFeature('beta_tools')) {
  enableBetaUI()
}

Combined Validate Response

Both features and variables are returned together in every /validate call:

json
{
  "valid": true,
  "expiresAt": "2027-01-01T00:00:00.000Z",
  "features": [
    { "name": "pro_mode",   "enabled": true  },
    { "name": "beta_tools", "enabled": false }
  ],
  "variables": {
    "serverUrl":  "https://prod.example.com",
    "maxRetries": "3",
    "debugMode":  "false"
  }
}

API Keys

Create scoped API keys for programmatic access to your products and license keys - useful for CI/CD pipelines, dashboards, and automation scripts.

Treat like a password

API keys are tied to your account. Never commit to source control. Rotate immediately if compromised. Use the minimum scopes your integration needs.

Creating an API Key

Go to Dashboard → Settings → API Keys → Create New API Key. The raw key is shown once only - copy it before closing the dialog.

Sending Requests

Include the key in one of these headers on every API request:

bash
# Option A - dedicated header (recommended)
curl https://api.astraguard.io/products \
  -H "X-API-Key: ag_your_key_here"

# Option B - Authorization header
curl https://api.astraguard.io/products \
  -H "Authorization: ApiKey ag_your_key_here"

Scopes

Each key can have one or more scopes. JWT session tokens (browser login) always have full access - scopes only apply to API keys.

ScopeWhat it covers
keys:readList keys, get stats, export CSV
keys:writeGenerate, revoke, freeze, batch operations
products:readList products, features, variables
products:writeCreate/update products, manage features & variables
webhooks:readList webhooks, view delivery history
webhooks:writeCreate, update, test webhooks
analytics:readKey stats and activation data

Error: Insufficient Scope

If a key tries to call an endpoint it doesn't have scope for, you'll receive:

json
{
  "error": "insufficient_scope",
  "message": "API key is missing required scope: keys:write",
  "required": "keys:write",
  "granted": ["keys:read", "products:read"]
}

Example: CI/CD pipeline

Create a key with only keys:write and products:read for a deploy script that generates license keys:

bash
# Generate 50 keys for a new release
curl -X POST https://api.astraguard.io/products/$PRODUCT_ID/keys \
  -H "X-API-Key: $ASTRAGUARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"count": 50, "type": "perpetual"}'

Rotating API Keys

Go to Settings → API Keys, click Delete on the old key, create a new one, and update your integrations. The old key stops working immediately.

Validation

Call POST /validate on every launch of your software to check license validity and receive features & variables.

Endpoint

POST/validate

Validate a license key. Call on every application launch.

json
{
  "key":       "XXXX-XXXX-XXXX-XXXX",
  "hwid":      "DESKTOP-ABC123_jdoe",
  "productId": "your-product-uuid",
  "version":   "1.2.0"
}

Request Fields

FieldRequiredDescription
keyrequiredThe license key string
hwidrecommendedHardware identifier for binding
productIdrequiredUUID of the product
versionoptionalClient version for forced update checks

productId is not your API key

These are two different values. Your Product ID is a UUID (e.g. ddc2fb44-a8e4-425b-8bf8-2d1565f6aed0) found under Dashboard → Product → Settings. Your API key starts with ag_ and is only used for authenticating dashboard/management API calls (creating keys, managing products) - never send it as productId. Sending an API key here will fail with ERR_INVALID_PRODUCT.

Success Response

json
{
  "valid":     true,
  "expiresAt": "2027-01-15T00:00:00.000Z",
  "features":  [{ "name": "pro", "enabled": true }],
  "variables": { "serverUrl": "https://prod.example.com" }
}

Failure Response

json
{ "valid": false, "reason": "hwid_mismatch" }

Failure Reasons

ReasonMeaning
invalid_licenseKey does not exist
license_expiredPast expiry date
hwid_mismatchHWID doesn't match bound HWID
hwid_bannedHWID is on the blacklist
license_revokedRevoked by developer
license_frozenTemporarily frozen
license_bannedBanned for abuse
version_too_oldBelow minVersion
vm_detectedVM detected (blockVm enabled)
debugger_detectedDebugger attached (blockDebug enabled)

SDK Example

typescript
const result = await guard.validate(licenseKey, {
  hwid:    getMachineId(), // ''
  version: '1.2.0',
})

if (!result.valid) {
  if (result.reason === 'version_too_old') showUpdateDialog()
  else showError('License: ' + result.reason)
  process.exit(1)
}

const hasPro = result.features?.find(f => f.name === 'pro')?.enabled
if (hasPro) enableProFeatures()

Activation

Call POST /activate the very first time a customer uses a key - this binds it to their hardware. All subsequent launches use /validate.

Endpoint

POST/activate

First-time activation - binds HWID to the key. Only needed once per device.

json
{
  "key":       "XXXX-XXXX-XXXX-XXXX",
  "hwid":      "DESKTOP-ABC123_jdoe",
  "productId": "your-product-uuid",
  "version":   "1.2.0"
}

Activation vs Validation

/activate/validate
WhenFirst launch onlyEvery subsequent launch
HWIDBinds HWID to keyVerifies HWID matches
Status changeunused → activeNone
After HWID resetAccepts new HWID on next activationRequires re-activation first

Recommended pattern

In practice you rarely need to call both: POST /validate already binds the HWID on a key's first use, the same way /activate does. A distinct /activate endpoint exists for callers who want an explicit first-run step (or its slightly different response shape) - none of the official SDKs currently chain validate() into activate() automatically, so call whichever one matches the flow you're building.

C++ Example

cpp
#include "astraguard.hpp"

AstraGuard::Client ag(
    "https://api.astraguard.io",
    AG_OBFSTR("OBFUSCATED_PRODUCT_ID")
);
ag.setResponseKey(AG_OBFSTR("YOUR-RESPONSE-KEY-BASE64"));

// HWID is bound automatically on first use - activate() takes only the license key.
auto r = ag.activate(AG_OBFSTR("USER_LICENSE_KEY"));

if (!r.success) {
    MessageBoxA(NULL, r.message.c_str(), "Activation Failed", MB_OK);
    ExitProcess(1);
}

Downloads & SDKs

Integrate AstraGuard into your application using any of the official SDKs below.

TypeScript / Node.js

npm @astraguard/sdk v1.3.0

Official TypeScript SDK - ships as both CJS and ESM. Works in Node.js, Electron, and bundled browser apps. View on npm

bash
npm install @astraguard/sdk
typescript
import { createClient } from '@astraguard/sdk'

const guard = createClient({
  apiUrl:    'https://api.astraguard.io',
  productId: 'your-product-uuid',
})

const result = await guard.validate('XXXX-XXXX-XXXX-XXXX')
if (result.valid) {
  console.log('License OK, expires:', result.expiresAt)
  const hasPro = result.features.find(f => f.name === 'pro')?.enabled
}

C# / .NET

NuGet AstraGuard.SDK v2.3.0

Official .NET SDK targeting .NET 8. Windows only for the security modules (Anti-Debug/Anti-VM/Anti-Dump); core license validation works cross-platform. View on NuGet

Pin your Target Framework to net8.0

New projects created with dotnet new default to whatever .NET SDK you have installed (e.g. net10.0). Set <TargetFramework>net8.0</TargetFramework> explicitly in your .csproj - referencing this package from a newer TFM throws BadImageFormatException before any of your code runs.

bash
dotnet add package AstraGuard.SDK
csharp
using AstraGuard.SDK;

var client = new AstraGuardClient(
    apiUrl:    "https://api.astraguard.io",
    productId: "your-product-uuid"
);

// Validates and exits the process if the license is invalid.
// Shows a visible error dialog on Windows - requires an interactive
// desktop session (do not call this from a headless/service process).
client.VerifyOrExit(licenseKey, hwid: Environment.MachineName);

// Or handle manually
var result = await client.VerifyLicense(licenseKey, hwid: Environment.MachineName);
if (!result.Valid)
{
    Console.Error.WriteLine($"License invalid: {result.Error}");
    Environment.Exit(1);
}

Client-side protection has limits - a .NET obfuscator is not encryption

ConfuserEx and Anti-Debug/Anti-VM raise the cost of tampering, but a determined attacker with a decompiler (dnSpy, ILSpy) can still recover logic and embedded secrets from a compiled assembly given enough time - no client-side SDK makes this mathematically impossible. Combine with a commercial protector (Themida/VMProtect) for real reverse-engineering resistance, and enable Block VM / Block Debugger / Integrity Check on your product so enforcement also happens server-side.

Python

pip astraguard v1.3.0

Official Python SDK for Flask, Django, FastAPI, and scripts. Sync + async clients, AES-256-GCM offline cache, heartbeat, and nonce-based response verification (anti-replay). Server-side use only. View on PyPI

bash
pip install astraguard
python
from astraguard import AstraGuardClient

client = AstraGuardClient(
    api_url="https://api.astraguard.io",
    product_id="your-product-uuid",
    response_auth_key="your-key-from-dashboard",  # enables response verification
)

result = client.validate("XXXX-XXXX-XXXX-XXXX")
if not result.valid:
    print("License invalid:", result.reason)
    exit(1)

print("License valid! Features:", [f.name for f in result.features if f.enabled])

C++ (Header-only)

Header-only astraguard.hpp v2.1.1

Single-header C++ library with Anti-Debug, Anti-VM, HMAC response verification, binary integrity, TLS certificate pinning and keyed string obfuscation. Drop it into your project - no build system integration required.

Download astraguard.hpp

cpp
#include "astraguard.hpp"

int main() {
    // AG_OBFSTR wraps a string literal so only obfuscated bytes exist in the
    // shipped binary - use it for the product ID and the response key below.
    AstraGuard::Client ag(
        "https://api.astraguard.io",
        AG_OBFSTR("your-product-uuid")
    );

    // Response Key from Dashboard -> Products -> Response Key. Enables
    // HMAC-SHA256 verification of every /validate reply, so a forged or
    // replayed response (e.g. from a proxy/MITM) is rejected instead of
    // silently accepted. Obfuscate it the same way as the product ID.
    ag.setResponseKey(AG_OBFSTR("your-response-key"));

    if (!ag.validate(AG_OBFSTR("XXXX-XXXX-XXXX-XXXX"))) {
        std::cerr << "Invalid: " << ag.getError() << std::endl;
        return 1;
    }

    bool hasPro = ag.hasFeature("pro");
    return 0;
}

Dependencies

On Windows the SDK uses WinHTTP - it ships with Windows, so there is no HTTP library to install or bundle. JSON parsing uses nlohmann/json (vcpkg install nlohmann-json). On Linux/macOS - or to opt back into libcurl on Windows - define AG_USE_CURL and link libcurl.

Client-side protection has limits - obfuscation is not encryption

AG_OBFSTR() and response signing raise the cost of tampering, but a determined attacker with a decompiler (increasingly AI-assisted) can still extract secrets from a compiled binary given enough time. No client-side SDK - ours or anyone else's - can make this mathematically impossible. Two things actually matter: (1) pack your final compiled binary with Themida/VMProtect/Enigma - that is what raises reverse-engineering cost, not the header source; (2) enable Block VM / Block Debugger / Integrity Check on your product so the real enforcement happens server-side, where a patched client can't reach it.

Rust

crates.io astraguard v1.2.0 New

Native Rust SDK with full Anti-Debug (IsDebuggerPresent, NtQueryInformationProcess, Frida detection), Anti-VM (CPUID, MAC OUI, process scan), AES-256-GCM offline cache (24h grace period), background heartbeat and HMAC response verification. All sensitive strings are XOR-obfuscated at compile time - no plaintext detection markers in the binary.

Windows Only

Anti-Debug, Anti-VM and HWID features use Win32 APIs. The SDK compiles on Linux/macOS but protection features are no-ops on non-Windows platforms.

bash
# Add to Cargo.toml
cargo add astraguard
# Also add tokio runtime
cargo add tokio --features full
rust
use astraguard::AstraGuardClient;

#[tokio::main]
async fn main() -> Result<(), Box> {
    let mut client = AstraGuardClient::new(
        "https://api.astraguard.io",
        "YOUR-PRODUCT-ID",
    )?
    // Enables nonce-based response verification (anti-replay / anti-MITM)
    .with_response_auth("your-key-from-dashboard")?;

    // Enable anti-debug + anti-VM checks on every validate call
    client.set_auto_enforce_security(true);

    // Validate - falls back to the AES-256-GCM offline cache if the server
    // is unreachable (24h grace period, HWID-bound)
    let details = client.verify_license("XXXX-XXXX-XXXX-XXXX").await?;
    if !details.raw.valid {
        eprintln!("License invalid");
        std::process::exit(1);
    }
    println!("License valid! Offline: {}", details.is_offline);
    Ok(())
}

Offline Cache

On first successful validation, the response is encrypted with AES-256-GCM (key bound to your HWID + product ID) and stored locally. If the server is unreachable, the SDK automatically falls back to the cached result for up to 24 hours.

Client-side protection has limits - obfuscation is not encryption

XOR string obfuscation and response signing raise the cost of tampering, but a determined attacker with a decompiler can still extract secrets from a compiled binary given enough time - no client-side SDK makes this mathematically impossible. Pack your final compiled binary with Themida/VMProtect for real reverse-engineering resistance, and enable Block VM / Block Debugger / Integrity Check on your product so enforcement also happens server-side.

C (Header-only)

Header-only astraguard.h v1.3.0

Single-header C89/C99 library - no C++, no classes, drops into any C project. Define ASTRAGUARD_IMPLEMENTATION in exactly one translation unit before including.

Download astraguard.h

c
// In ONE .c file only:
#define ASTRAGUARD_IMPLEMENTATION
#include "astraguard.h"

int main(void) {
    ag_client_t* client = ag_create(
        "https://api.astraguard.io",
        "your-product-uuid"
    );

    // Required - without a Response Key, validate()/activate() always
    // return valid=0 (fail closed). Get this from Dashboard -> Products
    // -> Security -> Response Key.
    ag_set_response_key(client, "your-response-key-base64");

    // Validates and calls exit(1) if invalid
    ag_validate_or_exit(client, license_key, "License Error");

    // Or inspect the result
    ag_result_t r = ag_validate(client, license_key, hwid);
    if (!r.valid) {
        fprintf(stderr, "License error: %s\n", r.reason);
        ag_destroy(client);
        return 1;
    }

    ag_destroy(client);
    return 0;
}

Dependencies

Requires libcurl for HTTP requests - link with -lcurl on Linux/macOS or add the curl import library on Windows. Also requires a JSON parser: cJSON by default, or swap in your own via the AG_JSON_* macros.

SDK Comparison

SDK Language Install Platforms
@astraguard/sdk TypeScript / JS npm Node.js, Electron, Browser
AstraGuard.SDK C# / .NET NuGet Windows, Linux, macOS
astraguard Python pip (PyPI) Windows, Linux, macOS
astraguard.hpp C++ Header-only Windows (+ partial Linux/macOS)
astraguard New Rust cargo (crates.io) Windows (Anti-Debug/VM), all platforms (validate)
astraguard.h C Header-only Windows, Linux, macOS

Response Key

The Response Key lets your client software verify that a validation response was genuinely produced by AstraGuard - preventing MITM attacks that spoof "valid": true.

Anti-MITM protection

Without response verification, a proxy could intercept the API response and replace it with {"valid":true}. The Response Key makes this forgery detectable.

Retrieving the Response Key

GET/products/:id/response-key

Returns the HMAC-SHA256 key for this product. Requires developer auth. Key is derived deterministically - never stored in the database.

json
{
  "productId": "your-product-uuid",
  "key": "a3f8c2d1e4b7...",
  "algorithm": "HMAC-SHA256",
  "usage": "Pass to your SDK's response-key setter (see the Downloads & SDKs page for the exact call per language)"
}

Verifying in TypeScript

typescript
import crypto from 'crypto'

function verifyResponse(body: string, sig: string, key: string): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', key)
    .update(body)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
}

const res  = await fetch('https://api.astraguard.io/validate', { ... })
const body = await res.text()
const sig  = res.headers.get('X-AstraGuard-Signature') ?? ''

if (!verifyResponse(body, sig, YOUR_RESPONSE_KEY)) {
  console.error('Signature invalid - possible MITM attack!')
  process.exit(1)
}
const data = JSON.parse(body)

Embed securely in binaries

XOR-obfuscate the Response Key in your compiled binary using a compile-time XOR key embedded in the header. Never ship it as a plain string - it would be visible via the strings tool.

HWID Reset

When a customer replaces their hardware, they need their license unbound. The developer approves the reset request from their dashboard - a 30-day cooldown applies between resets.

Customer Flow

  1. Customer logs into the Customer Portal with their license key
  2. Goes to Hardware tab → Request HWID Reset
  3. Enters a reason (e.g. "New PC build")
  4. Request appears in the developer's dashboard under HWID Resets
  5. Developer approves → key set to RESET-PENDING
  6. Customer activates on new hardware → new HWID bound, reset finalized

Cooldown & Eligibility

json
{
  "canReset":          false,
  "nextResetDate":     "2026-05-18T12:00:00.000Z",
  "hasPendingRequest": false,
  "lastReset": {
    "completedAt": "2026-04-18T12:00:00.000Z",
    "reason":      "New PC build"
  }
}

Dashboard Actions

POST/hwid-resets/:id/approve

Approve reset - sets key to RESET-PENDING.

POST/hwid-resets/:id/reject

Reject reset - no change to the key.

POST/license/:keyId/reset-hwid

Force-reset a key's HWID immediately (no customer request required).

Releases & Updates

Upload builds to Cloudflare R2. Customers download through the Customer Portal. Your software can check for updates automatically.

Uploading a File

Go to Products → Files → Upload. Supported: .exe .dll .zip .rar .7z .msi .apk .dmg .pkg .tar.gz. Max size: 100 MB.

Creating a Release

POST/products/:id/releases
json
{
  "version":       "2.1.0",
  "changelog":     "Bug fixes and performance improvements.",
  "is_prerelease": false,
  "file_id":       "uuid-of-uploaded-file"
}

Checking for Updates

GET/products/:id/check-update?version=1.0.0
json
{
  "hasUpdate":     true,
  "latestVersion": "2.1.0",
  "downloadUrl":   "https://r2.astraguard.io/...",
  "changelog":     "Bug fixes and performance improvements."
}

Auto-Update Pattern

typescript
const check = await fetch(
  `https://api.astraguard.io/products/${PRODUCT_ID}/check-update?version=${APP_VERSION}`
).then(r => r.json())

if (check.hasUpdate) {
  const ok = await showUpdateDialog(check.latestVersion, check.changelog)
  if (ok) openUrl(check.downloadUrl)
}

Customer Portal

The Customer Portal lets end-users manage their license without creating an AstraGuard account - they authenticate with their license key directly.

Authentication

POST/customer/auth
json
{
  "key": "XXXX-XXXX-XXXX-XXXX"
}

Returns a 2-hour JWT for web portal access.

Available Endpoints

FeatureEndpoint
License info & statusGET /customer/license
HWID reset eligibilityGET /customer/hwid/info
Request HWID resetPOST /customer/hwid/reset
Browse & download filesGET /customer/files
Release listGET /customer/releases
Remote variablesGET /customer/variables
AnnouncementsGET /customer/announcements

License Info Response

json
{
  "status":    "active",
  "key":       "XXXX-XXXX-XXXX",
  "product":   { "name": "My Software", "imageUrl": "..." },
  "features":  [{ "name": "pro", "enabled": true }],
  "expiresAt": null,
  "hwidBound": true,
  "hwidReset": {
    "canReset":          true,
    "nextResetDate":     null,
    "hasPendingRequest": false
  }
}

HWID-Bound Access

When a customer opens the portal via the Launcher, their Hardware ID is automatically detected and passed to the portal. The portal can then show device-specific information - such as whether the current machine matches the bound HWID - without requiring the customer to enter anything manually.

Portal vs. in-app auth

Portal login is for web management and does not perform HWID validation. In-app SDK calls (via POST /validate) always perform full HWID validation. Both methods issue short-lived sessions tied to the license key.

Launcher

The Launcher is a lightweight Windows component customers install once. After that, a single click in the Customer Portal fetches, decrypts, and runs your protected application - entirely in memory, never written to disk.

MAX plan only

The Launcher feature is only available on the MAX plan. Developers on the free Starter plan can still use direct SDK validation without the Launcher.

How it works

  1. Customer installs the Launcher component - one time only
  2. Customer clicks Launch in their Customer Portal
  3. A short-lived secure token is generated server-side
  4. The Launcher fetches the encrypted binary from AstraGuard servers
  5. The binary is decrypted and executed entirely in memory
  6. All intermediate buffers are securely wiped immediately after launch

vs. Encrypted Binary Delivery

Encrypted Binary Delivery downloads an encrypted .exe to the customer's machine - the file lands on disk and is decrypted on first run. The Launcher goes further: the binary never touches disk at all. Use Launcher when you want maximum protection against extraction and static analysis.

Setting up Launcher for your product

  1. Go to your product in the Dashboard → Security tab
  2. Scroll to the Launcher section and toggle it ON
  3. Set the App Display Name - shown on the Launch button in the portal
  4. Upload your .exe in File Manager
  5. Copy the File ID from File Manager and paste it into the Payload File field
  6. Save - the Launch button appears in your customers' portals immediately

File not visible in Downloads

Files distributed via Launcher do not appear in the customer's Downloads section of the portal. This is intentional - the file is only accessible through the protected launch flow, not as a direct download.

Customer setup

Provide customers with the Launcher installer. They run it once to set up the component on their machine. After that, clicking Launch in their portal starts the app automatically - no further steps needed on their end.

Security guarantees

GuaranteeDetail
Never written to diskThe protected binary is decrypted and executed entirely in memory
Short-lived tokensLaunch tokens expire after a very short window and are single-use
Encrypted in transitPayload is encrypted end-to-end between server and Launcher
HWID-verifiedServer validates the customer's hardware before streaming the payload
Anti-analysisLauncher exits silently if a debugger or analysis tool is detected
Memory cleaned upAll decryption buffers are securely wiped after execution starts

Webhooks

Webhooks notify your server when events happen in AstraGuard. Configure a URL and select the events you care about.

Creating a Webhook

POST/webhooks
json
{
  "url":    "https://yourserver.com/webhook",
  "events": ["license.activated", "license.revoked", "fraud.detected"],
  "secret": "your-webhook-secret"
}

Available Events

EventFired When
license.createdKey generated (single or bulk)
license.activatedFirst activation - HWID bound
license.validatedEvery successful validation
license.revokedKey revoked by developer
license.expiredKey passes expiry date
license.frozenKey frozen
license.unfrozenKey unfrozen
key.generatedBulk key generation completed
key.usedKey used (activation or validation)
product.createdProduct created
product.updatedProduct settings changed
product.deletedProduct permanently deleted
hwid.resetHWID reset requested by customer
fraud.detectedFraud alert triggered
hwid.mismatchWrong HWID used on validation

Verifying Signatures

typescript
import crypto from 'crypto'
import express from 'express'

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig      = req.headers['x-astraguard-signature'] as string
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(req.body)
    .digest('hex')

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
    return res.status(401).send('Invalid signature')

  const event = JSON.parse(req.body)
  console.log('Event:', event.type, event.data)
  res.sendStatus(200)
})

Delivery History

Every outgoing POST is logged. Inspect with GET /webhooks/:id/deliveries:

json
[
  {
    "id": "del_abc123",
    "webhookId": "wh_xyz789",
    "event": "license.activated",
    "statusCode": 200,
    "durationMs": 142,
    "success": true,
    "createdAt": "2026-05-24T10:15:00.000Z"
  },
  {
    "id": "del_def456",
    "event": "license.revoked",
    "statusCode": 500,
    "success": false,
    "createdAt": "2026-05-24T09:01:00.000Z"
  }
]

Full payload shapes

See the for complete JSON payload examples for every event type, plus signature verification code in Node.js, Python and C#.

Announcements

Publish announcements to your customers through the Customer Portal. Target all customers or only yours.

Creating an Announcement

POST/announcements
json
{
  "title":   "Scheduled Maintenance",
  "content": "API down for 30 min on Sunday 2am UTC.",
  "target":  "my_customers",
  "type":    "warning"
}

Target Options

TargetVisible To
allEvery customer on the platform
customersAll customers (alias for all)
my_customersOnly customers using your products

Reading Announcements

GET/announcementsAuth

List all announcements visible to the current user. Customers only see entries matching their product owner or target all.

Response

json
[
  {
    "id":        "uuid",
    "title":     "Scheduled Maintenance",
    "content":   "API down for 30 min on Sunday 2am UTC.",
    "type":      "warning",
    "target":    "my_customers",
    "createdBy": "developer-uuid",
    "createdAt": "2026-05-10T08:00:00Z",
    "readAt":    null
  }
]

Unread Count

GET/announcements/unreadAuth

Returns the count of unread announcements for the current user.

json
{ "count": 2 }

Mark as Read

POST/announcements/:id/readAuth

Mark a specific announcement as read. Sets readAt to current timestamp.

Announcement Types

TypeBadge ColorUse Case
infoBlueGeneral news, feature announcements
warningAmberScheduled downtime, deprecations
successGreenResolved incidents, new releases
dangerRedActive incidents, critical security notices
My Customers filtering: Use target: "my_customers" to send announcements that only appear for customers who own a license tied to your developer account. This lets you communicate with your user base without broadcasting to the entire platform.

Security Best Practices

License validation is a target for attackers. Follow these practices to make bypassing AstraGuard significantly harder.

1. Validate Server-Side

Never trust client-side license checks alone. Perform validation on your backend and gate access to sensitive functionality or data there. A client-side check can be patched in memory.

2. Verify Response Signatures

Use the Response Key to verify /validate responses haven't been intercepted and replaced. See the Response Key page for implementation.

3. Obfuscate Credentials

cpp
// XOR-obfuscate product ID and response key at compile time (key defined in AstraGuard.h)
// AstraGuard.h does this automatically
// Never ship plain credential strings in binaries

4. Enable HWID Binding

Without HWID binding, a key can be used on any machine simultaneously. Enable it so licenses are tied to specific hardware.

5. Enable Anti-Debug & Anti-VM

Turn on blockDebug and blockVm in product settings to prevent reverse engineering in isolated environments.

6. Re-validate Periodically

typescript
// Re-check every hour while the app is running
setInterval(async () => {
  const result = await guard.validate(licenseKey)
  if (!result.valid) shutdownApplication()
}, 60 * 60 * 1000)

Pre-commit checklist

  • No hardcoded secrets in source
  • .env in .gitignore
  • Response signature verification implemented
  • Rate limiting on validation endpoints

Anti-Tampering

Protect your software against patching, debugging, and execution in analysis environments.

Product Settings

SettingWhat It Does
Block DebuggersReturns debugger_detected if a debugger is attached at activation
Block VMsReturns vm_detected if running inside a virtual machine
Integrity CheckVerifies binary SHA-256 hash matches integrityHash stored on the product

Update hash on every release

Every time you ship a new version, update integrityHash in your product settings or legitimate users will be blocked.

Obfuscation Recommendations

  • XOR-obfuscate credential strings - use a compile-time XOR key embedded in the header
  • Consider Themida / VMProtect for critical validation code paths
  • Split validation logic across multiple functions to resist static patching
  • Add anti-debug checks at multiple points, not just startup
  • Strip symbols and enable LTO in your release build - see the Hardening Guide's "Compiler-Level Release Hardening" section for per-language flags

Error Codes

Standard HTTP status codes plus a reason field for license-specific errors.

HTTP Status Codes

CodeMeaning
200Success - check valid field for license status
400Bad Request - missing or invalid parameters
401Unauthorized - missing or expired JWT/API key
403Forbidden - insufficient permissions
404Not Found - resource does not exist
429Too Many Requests - rate limit exceeded
500Internal Server Error
503Service Unavailable - maintenance mode

License Reason Codes

These are returned as { "valid": false, "reason": "..." } with HTTP 200 - the request succeeded, but the license check failed.

ReasonHTTPCauseSuggested User Message
invalid_license200Key does not exist"Invalid license key"
license_expired200Past expiry date"Your license has expired"
license_revoked200Revoked by developer"License revoked - contact support"
license_frozen200Temporarily disabled"License temporarily suspended"
license_banned200Banned for abuse"License banned"
hwid_mismatch200HWID doesn't match bound value"Hardware mismatch - request HWID reset"
hwid_banned200HWID is blacklisted"This device is banned"
version_too_old200Below minVersion"Please update your software"
vm_detected200Virtual machine detected"VMs are not supported"
debugger_detected200Debugger attached"Debugger detected"
ip_banned200IP is blacklisted"Your IP is banned"
product_not_found200Unknown product ID"Unknown product"

Authentication & Authorization Errors

ErrorHTTPCause
unauthorized401Missing or expired JWT / API key
invalid_token401Malformed or tampered token
forbidden403Role doesn't have permission for this action
insufficient_scope403API key is missing a required scope
email_not_verified403Account email not yet verified

Validation & Resource Errors

ErrorHTTPCause
missing_field400Required body field not provided
invalid_field400Field value failed validation (e.g. bad URL)
not_found404Resource doesn't exist or you don't own it
conflict409Duplicate resource (e.g. feature name already exists)
plan_limit_reached403Current plan doesn't allow more of this resource

Rate Limit Errors

ErrorHTTPCause
too_many_requests429Per-minute or per-day quota exceeded

The Retry-After header (seconds) and RateLimit-Reset (Unix timestamp) tell you when to retry. See the Rate Limits page for full details.

Error Response Format

json
// HTTP 200 - license check failed (not an HTTP error):
{ "valid": false, "reason": "hwid_mismatch" }

// HTTP 4xx - API-level error:
{ "error": "unauthorized", "message": "Invalid or expired token" }

// HTTP 403 - missing API key scope:
{
  "error": "insufficient_scope",
  "message": "API key is missing required scope: keys:write",
  "required": "keys:write",
  "granted": ["keys:read", "products:read"]
}

// HTTP 429 - rate limited:
{ "error": "too_many_requests", "retryAfter": 60 }

Rate Limiting

AstraGuard applies rate limits to protect platform stability. Limits depend on your plan and the endpoint type.

API Key Limits by Plan

PlanPer minutePer day
Starter (free)30 requests1,000 requests
Max600 requests100,000 requests

Public Endpoint Limits

EndpointLimitWindow
/validate, /activate100 requests1 minute per IP
/login, /register10 requests15 minutes per IP

Rate Limit Headers

Every response includes these headers - use them to self-throttle your integration:

bash
RateLimit-Limit:     600
RateLimit-Remaining: 587
RateLimit-Reset:     1713441600   # Unix timestamp (seconds)

Handling 429 Too Many Requests

typescript
async function validateWithRetry(key: string, retries = 3) {
  for (let i = 0; i < retries; i++) {
    const res = await fetch('https://api.astraguard.io/validate', { /* ... */ })
    if (res.status === 429) {
      const resetAt = parseInt(res.headers.get('RateLimit-Reset') ?? '0') * 1000
      const waitMs = Math.max(resetAt - Date.now(), 1000)
      await new Promise(r => setTimeout(r, waitMs))
      continue
    }
    return res.json()
  }
  throw new Error('Rate limit exceeded after retries')
}

FAQ

Frequently asked questions about AstraGuard.

Can a license be used on multiple devices?

No. Once activated, the key is bound to that device's HWID. To move it, the customer requests an HWID reset through the Customer Portal - the developer approves it from their dashboard, with a 30-day cooldown.

What is the HWID format?

AstraGuard uses a stable machine-derived identifier. The exact format is provided in the SDK header to prevent public documentation of the construction method.

How do I force users to update?

Set minVersion on your product. When the client sends a version field below minVersion in the validate request, the API returns { "valid": false, "reason": "version_too_old" }.

Are validation responses signed?

Yes. Every response includes an X-AstraGuard-Signature header (HMAC-SHA256). Use the Response Key from your product to verify it - see the Response Key page.

What happens when a subscription expires?

The key's expires_at passes and /validate returns { "valid": false, "reason": "license_expired" }. Extend the expiry via the dashboard or API.

Can I generate keys programmatically?

Yes. Use POST /products/:id/keys with your API key in the Authorization header - useful for automating key generation after a payment webhook.

What plans are available?

AstraGuard offers a free Starter plan and a Max plan ($4.99/month) with unlimited products, unlimited keys, and all features.

How do I contact support?

Open a ticket in your dashboard under Support → New Ticket, or join the community on Discord for live help.

Hardening Guide

A checklist-driven guide for making your software as crack-resistant as possible using AstraGuard's built-in protection layers - plus implementation patterns every developer should follow.

This page is for developers

These are the practices you should follow to protect your software. They cover configuration, architecture, and integration patterns - nothing here is useful to end-users.

Protection Checklist

Go through every item before shipping. Each unchecked box is an open door.

ItemWhere to configurePriority
HWID binding enabledSend hwid in every validate/activate requestCritical
Response signature verificationResponse Key page - verify X-AstraGuard-SignatureCritical
Credentials obfuscated in binaryUse AstraGuard.h or XOR-encode manuallyCritical
No offline fallbackIf API call fails → exit, never assume validCritical
Block debugger (blockDebug)Product Settings in dashboardHigh
Block VM (blockVm)Product Settings in dashboardHigh
Integrity check (integrityCheck)Product Settings → set integrityHash on each releaseHigh
Periodic re-validationRe-validate every 30-60 min while runningHigh
Version enforcement (minVersion)Product Settings → bump after each security fixMedium
Webhooks for anomaly alertsWebhooks page → subscribe to fraud.detected, hwid.mismatchMedium

1. HWID Binding - The Foundation

HWID binding is the single most effective control. A key without HWID can be freely shared - a bound key is locked to one device. Always send hwid in every request.

cpp
// Use the HWID helper from AstraGuard.h - implementation is intentionally not documented
std::string hwid = ag.getHWID();
auto r = ag.validate(licenseKey, hwid, APP_VERSION);
if (!r.valid) ExitProcess(1);

Never skip HWID on error

If your HWID collection fails, deny access - do not fall back to validating without a HWID. A missing HWID is a weaker binding.

2. Response Signature Verification

AstraGuard signs every response with HMAC-SHA256. Verify the X-AstraGuard-Signature header before trusting "valid": true. Without this check, a local proxy can spoof a valid response.

typescript
const res  = await fetch('https://api.astraguard.io/validate', { method: 'POST', body: JSON.stringify(payload) })
const body = await res.text()
const sig  = res.headers.get('X-AstraGuard-Signature') ?? ''

const expected = 'sha256=' + crypto.createHmac('sha256', RESPONSE_KEY).update(body).digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
  console.error('Signature mismatch - terminating')
  process.exit(1)
}
const data = JSON.parse(body)

3. Credential Obfuscation

Your Product ID and Response Key must never exist as plain strings inside your binary. Use compile-time XOR encoding - the C++ header does this automatically.

cpp
// AstraGuard.h handles this for you.
// The product ID is XOR-encoded at compile time and decoded at runtime only.
// Never paste credentials as raw string literals anywhere in your source code.

Keep credentials out of source control

Use environment variables or a secrets manager for Node/web projects. For C++ desktop apps, use compile-time obfuscation via the header - never plain strings.

4. No Offline Mode

The most common bypass is simply blocking the API call so the check never happens. Design your validation flow so that any failure - network error, timeout, unexpected response - results in the application refusing to run, not silently passing.

typescript
async function checkLicense(): Promise {
  let result: ValidateResult

  try {
    result = await guard.validate(licenseKey, { hwid, version: APP_VERSION })
  } catch (err) {
    // Network failure, timeout, etc. → DENY access
    showError('Could not reach license server. Check your connection.')
    process.exit(1)
  }

  if (!result.valid) {
    showError('License invalid: ' + result.reason)
    process.exit(1)
  }
}

// Good: call at startup AND periodically
await checkLicense()
setInterval(checkLicense, 45 * 60 * 1000) // re-check every 45 min

5. Keep Validation on the Critical Path

Validation must happen before your software's protected functionality runs - not in a background thread that can be ignored, not optionally skipped on first launch. Structure your startup sequence so the entire app is gated behind the license check returning valid.

typescript
async function main() {
  // ① License check - MUST complete before anything else
  await checkLicense()

  // ② Only reached if valid
  await initializeApp()
  renderUI()
}

6. Enable All Product Security Flags

Turn these on in Dashboard → Products → [Product] → Settings:

FlagWhat it does
blockDebugRejects activate/validate when a debugger is attached
blockVmRejects activate/validate from virtual machine environments
integrityCheckValidates binary SHA-256 hash at activation time - rejects tampered executables

Update integrityHash with every release

After building a new version, generate its SHA-256 hash and update integrityHash in your product settings before distributing the binary.

7. Version Enforcement - Ship Fixes Fast

When you find and fix a vulnerability or patch a bypass, bump minVersion immediately. All older builds will be rejected on the next validation call, forcing users to update to the patched version.

bash
# After shipping a security patch:
# 1. Update minVersion in Dashboard → Products → [Product] → Settings
# 2. Upload new build to File Manager
# 3. Create a new Release with the updated version number
# 4. All clients below minVersion will receive: { "valid": false, "reason": "version_too_old" }

8. Real-Time Anomaly Alerts via Webhooks

Subscribe to fraud.detected and hwid.mismatch webhook events so you're notified immediately when something suspicious happens - before it escalates.

json
{
  "url":    "https://yourserver.com/astraguard-events",
  "events": ["fraud.detected", "hwid.mismatch", "license.activated"],
  "secret": "your-webhook-secret"
}

9. Protect Sensitive Features with Feature Flags

Use feature flags to keep premium or sensitive functionality disabled by default. Features are returned per-validation - toggling them off in the dashboard takes effect on the next license check across all active users instantly, with no redeploy needed.

typescript
const result = await guard.validate(licenseKey, { hwid, version })

const features = result.features ?? []
const hasPro   = features.find(f => f.name === 'pro_mode')?.enabled === true

if (hasPro) {
  // Only runs if the server explicitly enables this flag for the key
  unlockProFeatures()
}

10. Compiler-Level Release Hardening

Separate from anything AstraGuard configures server-side: for compiled languages (C, C++, Rust), the SDK ships as source that your own compiler builds - so the hardening that matters is in your release build, not the SDK. None of this is required for correctness; it only reduces how much information (symbol names, debug info, local file paths) is visible to anyone inspecting your shipped binary.

LanguageRecommended flags
Rust[profile.release] in your app's Cargo.toml: strip = true, lto = true, codegen-units = 1
C / C++ (MSVC)/O2 /GL, linker /LTCG, /DNDEBUG, strip the .pdb from what you ship
C / C++ (GCC/Clang)-O2 -flto -s -DNDEBUG
C# / .NETNo action needed - the published NuGet package is already obfuscated (internal implementation only; public API stays stable). Your project's own Debug/Release setting doesn't change that.

Combine with the credential-obfuscation guidance in section 3 above - that protects your product ID and response key specifically, while these flags reduce what's visible across the rest of your binary.

Summary

Minimum required for production

  • HWID binding on every call
  • Response signature verified
  • Credentials XOR-obfuscated in binary
  • No offline fallback - fail closed
  • blockDebug + blockVm enabled

Need help hardening your specific integration? Join the AstraGuard Discord - other developers and the team are active there.

Encrypted Binary Delivery

Protect your compiled software from piracy by ensuring the plaintext executable never exists on a customer's disk unless they hold a valid, HWID-bound license. AstraGuard encrypts the binary server-side at upload time; a lightweight C++ loader decrypts it in RAM at launch time.

How it protects your software

Even if a customer shares ag_payload.enc, it is useless without a valid license key that passes HWID verification. The AES-256-GCM key is never stored anywhere - it is derived on demand only after the server validates the license.

How It Works

text
Developer uploads .exe  (Encrypted Delivery enabled)
  → Server encrypts with AES-256-GCM, stores ag_payload.enc in R2

Developer bundles for distribution:
  AstraGuardLoader.exe  ← renamed to YourSoftware.exe
  ag_config.json        ← product / file metadata
  ag_payload.enc        ← encrypted binary (safe to distribute)

Customer launches YourSoftware.exe
  → Loader reads ag_config.json
  → Loader asks for license key (or reads ag_license.key)
  → POST /files/:fileId/decrypt-key  { licenseKey, hwid, productId }
     → Server validates license + HWID binding
     → Returns AES key + IV (only if valid)
  → Loader decrypts ag_payload.enc in RAM
  → Loader writes decrypted .exe to %TEMP% with random name
  → Loader launches the temp file via CreateProcess()
  → Loader deletes temp file within ~1 second of launch
  → AES key zeroed from memory

Enabling Encrypted Delivery

  1. Go to Dashboard → Products → [Your Product] → Settings → Security
  2. Find the Encrypted Delivery panel and flip the toggle ON
  3. Re-upload your .exe - the server encrypts it automatically at upload time
  4. Download ag_config.json and ag_payload.enc from the File Manager tab
  5. Build the loader (see below) and rename it to your software's name
  6. Bundle all three files and distribute

Re-upload required when toggling

Enabling Encrypted Delivery does not retroactively encrypt previously uploaded files. You must re-upload your binary after enabling the toggle.

Building the Loader

The loader source is in sdk/loader/AstraGuardLoader.cpp. It uses only Windows SDK APIs (WinHTTP + BCrypt) - no external dependencies.

Prerequisites

  • CMake ≥ 3.16
  • MinGW-w64 (g++) or MSVC (Visual Studio 2019+)
  • Windows SDK (WinHTTP / BCrypt headers)

MinGW Build

bash
cd sdk/loader
mkdir build && cd build
cmake .. -G "MinGW Makefiles"
cmake --build . --config Release
# Output: build/AstraGuardLoader.exe

MSVC Build

bash
cd sdk/loader
mkdir build && cd build
cmake .. -G "Visual Studio 17 2022"
cmake --build . --config Release
# Output: build/Release/AstraGuardLoader.exe

Rename the compiled .exe to whatever your software is called before distributing.

ag_config.json

Place ag_config.json next to the loader. Download it from the File Manager tab in your dashboard.

json
{
  "apiUrl":      "https://api.astraguard.io",
  "productId":   "your-product-uuid",
  "fileId":      "file-uuid",
  "originalName":"YourSoftware.exe",
  "saveLicense": true
}
FieldDescription
apiUrlAstraGuard API base URL
productIdYour product UUID (from dashboard)
fileIdThe specific file UUID to decrypt
originalNameUsed as the temp-file name when launching
saveLicenseIf true, saves the key to ag_license.key so the user is not prompted again

Decrypt-Key API Endpoint

The loader calls this endpoint automatically. You do not need to call it manually unless building a custom loader.

http
POST /files/:fileId/decrypt-key
json
// Request body
{
  "licenseKey": "XXXX-XXXX-XXXX-XXXX",
  "hwid":       "",
  "productId":  "your-product-uuid"
}

// Success response
// Response: AES-256-GCM decryption parameters (format documented in the SDK).

// Failure (invalid / expired / HWID mismatch)
HTTP 401  { "error": "invalid_license" }

Encrypted File Format (ag_payload.enc)

The encrypted file format is handled automatically by the AstraGuard SDK - no manual parsing required.

AES-256-GCM authentication guarantees that any tampering with the encrypted payload is detected and decryption is aborted.

Security Properties

  • The AES-256-GCM key is never stored on disk - it exists in server memory only for the duration of the request and in loader memory only during decryption
  • The decrypted binary is written to %TEMP% under a randomised name and deleted within ~1 second of the child process starting
  • ag_payload.enc without a valid, HWID-bound license is completely useless
  • HWID binding means even a shared license key only decrypts on the bound machine
  • The loader itself can be packed with UPX, Themida, or VMProtect for additional reverse-engineering resistance

Zero plaintext on disk

Your compiled binary never touches the end-user's disk in decrypted form. The decryption happens entirely in RAM, and the temp file is deleted before the parent process exits.

Geo-Blocking & IP Whitelist

Restrict where your software can be activated and validated - by country or by specific IP address. Both checks run server-side on every /activate and /validate request. They cannot be bypassed by patching the client.

Geo-Blocking

Geo-blocking lets you restrict activations and validations to a specific set of countries. You define an allow-list of ISO 3166-1 alpha-2 country codes (e.g. US, DE, GB). Any request from a country not on the list is rejected.

Allow-list, not a block-list

Geo-blocking in AstraGuard works as a country allow-list. If you configure US,GB, only those two countries can activate or validate. All others are rejected. Leaving the list empty disables geo-blocking entirely.

Configuring Geo-Blocking

Enable geo-blocking in Product Settings → Security → Geo-Blocking. Enter the allowed country codes as a comma-separated list. The setting applies immediately to all existing and new keys for that product.

Error Response

When a request is rejected due to geo-blocking, the server returns:

json
// POST /activate - HTTP 403
{
  "error": "ERR_GEO_BLOCKED",
  "details": {
    "allowedCountries": ["US", "GB"],
    "requestCountry": "DE"
  }
}

// POST /validate - validate response format
{
  "valid": false,
  "reason": "ERR_GEO_BLOCKED"
}

Country Codes

Use ISO 3166-1 alpha-2 two-letter country codes. Examples:

text
US  United States
GB  United Kingdom
DE  Germany
FR  France
CA  Canada
AU  Australia
JP  Japan
NL  Netherlands

Handling ERR_GEO_BLOCKED in Your Software

typescript
const result = await guard.validate(licenseKey)
if (!result.valid) {
  if (result.reason === 'ERR_GEO_BLOCKED') {
    showError('This software is not available in your region.')
  } else {
    showError('License validation failed: ' + result.reason)
  }
  return
}
cpp
// C++ - after calling validate()
if (!result.valid) {
    if (result.reason == "ERR_GEO_BLOCKED") {
        MessageBoxA(nullptr, "This software is not available in your region.",
                    "Region Restricted", MB_OK | MB_ICONERROR);
    }
    ExitProcess(1);
}

IP Whitelist

The IP whitelist restricts a specific license key to one or more trusted IP addresses. Only requests originating from a whitelisted IP can activate or validate that key. This is useful for server-side or fixed-installation deployments where the IP is known in advance.

Per-key, not per-product

The IP whitelist is set on individual keys, not on a product. This lets you give different IP restrictions to different customers without changing global product settings.

Setting a Key's IP Whitelist

In the dashboard: expand a key row → Quick Actions → IP Whitelist. Or via API:

http
PUT /products/:productId/keys/:keyId/ip-whitelist
Authorization: Bearer <token>
Content-Type: application/json

{
  "ipWhitelist": "203.0.113.10,203.0.113.11"
}

Pass an empty string to remove the whitelist entirely:

json
{ "ipWhitelist": "" }

Error Response

json
// POST /activate - HTTP 403
{
  "error": "ERR_IP_NOT_WHITELISTED",
  "details": {
    "allowedIps": ["203.0.113.10", "203.0.113.11"]
  }
}

// POST /validate - validate response format
{
  "valid": false,
  "reason": "ERR_IP_NOT_WHITELISTED"
}

Combining Both

Geo-blocking and IP whitelisting can be active at the same time. A request must pass both checks to succeed - geo first, then IP. Use this combination for maximum lockdown on server-deployed licenses.

Do not use IP whitelist for end-user desktop software

Home users typically have dynamic IPs that change on router restart or ISP reassignment. IP whitelisting is intended for server deployments, CI runners, or fixed infrastructure - not consumer desktops.

Force Updates

Use minVersion to prevent outdated clients from running. When a client's version is below the minimum, validation is rejected instantly - server-side, no patch can bypass it.

How It Works

Every POST /validate request can include a version field. AstraGuard compares it against your product's minVersion setting using semver ordering. If the client is too old:

json
{ "valid": false, "reason": "version_too_old" }

Server-side enforcement

The version check happens on the server. A cracker cannot patch out the update check - the server simply refuses to validate.

Setting minVersion

  1. Go to Dashboard → Products → [Your Product] → Settings
  2. Set the Min Version field (e.g. 2.0.0)
  3. Save - takes effect immediately for all new validation requests

Sending Version in Your Client

TypeScript SDK

typescript
const result = await guard.validate('XXXX-XXXX-XXXX-XXXX', {
  hwid: getMachineId(),
  version: '2.1.0'   // your app's current version
})

if (!result.valid && result.reason === 'version_too_old') {
  showDialog('A new version is required. Please update.')
  process.exit(1)
}

C++ (REST)

cpp
// POST /validate body
std::string body = R"({
  "key":       "XXXX-XXXX-XXXX-XXXX",
  "hwid":      ")" + hwid + R"(",
  "productId": ")" + productId + R"(",
  "version":   "2.1.0"
})";

Recommended Client-Side Pattern

typescript
switch (result.reason) {
  case 'version_too_old':
    // Show update dialog, open download URL, then exit
    openBrowser('https://yoursite.com/download')
    exit(1)
  case 'license_expired':
    showMessage('Your license has expired. Please renew.')
    exit(1)
  // ...
}

Version Format

AstraGuard compares versions as semver strings (MAJOR.MINOR.PATCH). Examples:

Client VersionminVersionResult
2.1.02.0.0valid
1.9.92.0.0version_too_old
2.0.02.0.0valid

Always include version in validate calls

If your client does not send a version field, the check is skipped. Make sure every validate call includes the current app version.

Reseller System

Give trusted partners the ability to generate and distribute license keys for your products - without ever accessing your product settings, source code, or dashboard.

Architecture

text
Developer (you)
  └── invites Resellers
        └── Reseller has Quotas (per product + key type)
              └── Reseller generates Keys from quota
                    └── Reseller assigns Keys to Customers

Inviting a Reseller

Go to Dashboard → Resellers → Invite Reseller. There are two flows:

ScenarioFlow
Person already has an AstraGuard accountEnter their email → they receive a request to accept in their dashboard
New person, no account yetEnter email + name → they receive an invite link valid for 7 days → they register and are automatically linked as your reseller

Quotas

Quotas control exactly how many keys a reseller can generate per product and key type. You set them - the reseller can never exceed them.

FieldDescription
ProductWhich of your products the quota applies to
License Typeperpetual, subscription, or trial
AllocatedTotal keys the reseller may generate
UsedKeys generated so far (auto-tracked)

To grant more quota: Resellers → [Reseller] → Edit Quota.

What Resellers Can Do

  • Generate keys from their quota (up to 50 at a time)
  • Revoke keys they generated
  • Create and manage their own customer list
  • Assign keys to specific customers
  • View their reseller dashboard with stats

What Resellers Cannot Do

  • Access your product settings or security configuration
  • See keys generated by other resellers or by you directly
  • Exceed their allocated quota
  • Modify product features, variables, or webhooks

Keys are traceable

Every key generated by a reseller has reseller_id set. You can always see which reseller generated which key from your dashboard.

Trial Key Limits

When a reseller generates trial keys, duration is capped at 720 hours (30 days). They can set hours freely within that cap. Default is 7 days if not specified.

Reseller API (for custom integrations)

http
GET  /reseller/dashboard          → stats (quota used, keys generated, customers)
GET  /reseller/keys               → keys this reseller generated
POST /reseller/keys/generate      → generate keys from quota
GET  /reseller/customers          → customer list
POST /reseller/customers          → add customer
POST /reseller/customers/:id/assign → assign key to customer

Authenticate with the reseller's JWT token (obtained via normal POST /login).

Payments & Billing

AstraGuard Max is a simple $4.99/month subscription - no setup fee. Pay via Stripe (card) or crypto.

Pricing

ChargeAmountWhen
Monthly subscription$4.99 / monthStarts immediately on signup

No setup fee

$4.99/month, billed immediately on signup. No one-time setup fee. Cancel anytime.

Payment Methods

Stripe (Credit / Debit Card)

  1. Click Get Access on astraguard.io
  2. Select Pay with Card
  3. Complete Stripe checkout - your account is upgraded instantly
  4. Register with the same email used at checkout

Cryptocurrency

We accept BTC, ETH, and SOL.

  1. Click Get Access → Pay with Crypto
  2. Select your coin, send the exact amount to the displayed address
  3. Submit your transaction hash and email address
  4. Our team manually verifies and upgrades your account (typically within a few hours)

Include your email

Crypto payments require your email address at submission so we can link the payment to your account. Without it we cannot process the upgrade.

Plan Features (Max Plan)

  • Unlimited products
  • Unlimited key generation
  • Full API access
  • Webhooks, feature flags, remote variables
  • File distribution & encrypted delivery
  • Reseller system
  • Customer portal
  • Priority support

Managing Your Subscription

Stripe customers can manage their subscription (cancel, update card) via the Stripe billing portal:

http
POST /stripe/portal   → returns { url } to Stripe billing portal

Or go to Dashboard → Settings → Billing.

Referral Program

Refer other developers to AstraGuard and earn free Max plan months for every referral who becomes a paying subscriber.

How It Works

  1. Get your unique referral link from Dashboard → Settings → Referral
  2. Share it with other developers
  3. When a referred developer signs up and upgrades to the Max plan, you earn 1 free month of Max
  4. Free months are applied automatically to your next billing cycle

Reward: 1 free Max month per referral

Each confirmed referral adds one free month of the Max plan to your account. There is no cash payout - the reward is applied directly as plan credit.

Your Referral Link

Your referral link looks like this:

text
https://www.astraguard.io/register?ref=YOUR_CODE

Anyone who registers via this link is tracked as your referral. The code is automatically generated when you first open the Referral tab.

Tracking Your Referrals

The Referral tab in your dashboard shows:

FieldDescription
Referral CodeYour unique referral identifier
Total ReferralsNumber of developers who signed up via your link
Months EarnedFree months already applied to your plan
Months PendingConfirmed referrals awaiting application

API: Get Referral Stats

http
GET /referrals/me
Authorization: Bearer <token>
json
{
  "referralCode": "ABC123XY",
  "referralLink": "https://www.astraguard.io/register?ref=ABC123XY",
  "totalReferrals": 3,
  "monthsEarned": 2,
  "monthsPending": 1,
  "registrations": [
    { "email": "jo***@example.com", "joinedAt": "2026-05-10T12:00:00Z", "status": "paid" }
  ]
}

Requires a Developer account

The referral program is available to Developer accounts only. Customers and Resellers do not have access.

Android · Direct Download

AstraGuard,
in your pocket.

Licenses, products, and analytics for your AstraGuard account - on your phone. Same data, same real-time updates as the web dashboard.

Download APK
v1.0.0
Current version
~92 MB
Download size
Android 8+
Requirement

Developer accounts

The app is built for managing your own AstraGuard account - it is not the Customer Portal. End customers activating a license key should continue to use the web Customer Portal.

What's Inside

Dashboard

Quick actions, license overview (total / active / expired / revoked), and a summary of your products.

Licenses

Search, filter, add notes, freeze/unfreeze, revoke/unrevoke, and reset HWID on the go.

Products

View your products and their key and activation stats at a glance.

Analytics

Activation trends and your top-performing products, updated in real time.

Settings

Account info, Two-Factor Authentication, session management, and notification preferences.

Installation

  1. Tap Download APK at the top of this page
  2. Open the downloaded file on your Android phone. If prompted, allow "Install from unknown sources" for your browser or file manager - this is normal for any app installed outside Google Play
  3. Open the app and sign in with your existing AstraGuard account (email/password, Google, or GitHub)

Security

Biometric App Lock

Face ID / Fingerprint is required on every cold start and after the app has been backgrounded - on top of your regular login, not instead of it.

Secure Token Storage

Your session token lives in the OS-level secure keystore (Android Keystore), never in plain text.

Google & GitHub OAuth

Sign in the same way you do on the web, alongside email/password and Two-Factor Authentication.

Real-Time Sync

Connects over the same Socket.io channel as the web dashboard, so notifications and data stay in sync.

Updates

Small JavaScript-level updates (bug fixes, UI tweaks) can be delivered automatically over the air the next time you open the app - no reinstall needed. Larger updates (new native features) will require downloading a new APK from this page.

Discord Community

Join the AstraGuard Discord to get support, share feedback, and stay updated on new features.

Join the Server

discord.gg/Zkcyy5GnQd

After joining you will be in the Newbie role with limited access. Verify your AstraGuard account to unlock the full server.

Verifying Your Account

Link your AstraGuard account to Discord to get your plan role (Starter or Max) and unlock all channels.

  1. Join the server
  2. Go to the #verify channel
  3. Run the command: /verify email:you@example.com
  4. The bot checks your AstraGuard account and assigns your role automatically

Use your AstraGuard email

The email must match the one registered on astraguard.io. If you don't have an account yet, register first.

Roles

RoleWho
NewbieEveryone on join - limited access until verified
StarterVerified AstraGuard account on the Starter plan
MaxVerified AstraGuard account on the Max plan
MemberAssigned after verification alongside plan role

Getting Support

If you have a question or issue:

  • Use the #open-ticket channel to create a private support ticket
  • For general questions use the appropriate topic channel
  • For bug reports include your product ID, key prefix, and what endpoint you are calling

Changelog

Recent updates and improvements to the AstraGuard platform.

Stay updated

Join the Discord server for real-time update announcements.

August 2026

v1.2.0 - Inactive Account Storage Cleanup

  • Storage cleanup warnings - if your account goes unused for an extended period, you'll get a warning email listing exactly which Files and Releases are at risk before anything is touched. A grace period follows, and logging back in cancels it automatically - no other action needed
  • Active products are always protected - any product with recent customer license validations is automatically excluded from cleanup, regardless of your own login activity. This only ever reaches products with no real usage left

July 2026

v1.1.0 - Tier Entitlements & SDK Updates

  • Tier Entitlements - Give a single product multiple pricing tiers. Configure extra features and remote variables per license-key tier - however you name your tiers - on top of your existing base Features/Variables. Opt-in per product, configurable in the new Tier Entitlements tab under Product Settings
  • New C SDK - a single-header C89/C99 client alongside the existing C++ header, for projects that can't use C++. Available now under Downloads & SDKs
  • Updated SDKs - TypeScript 1.3.0, C# 2.3.0, Python 1.3.0, Rust 1.2.0, C++ 2.1.0. Includes fixes for false-positive Anti-VM/integrity detections that could affect legitimate machines, a crash in the C# Anti-Dump module, and more reliable update checks
  • Dashboard fix - the Tier Entitlements toggle no longer appears to reset after saving other Security settings

v1.0.0 - AstraGuard Mobile App (Android)

  • Manage your account from your phone - a companion Android app covering dashboard overview, license management, product management, and analytics, all synced in real time via the same API and Socket.io connection as the web dashboard
  • Biometric app lock - Face ID / Fingerprint is required on cold start and whenever the app returns from the background, not just as an alternative login method
  • Push notifications for license activity and account alerts
  • Direct APK download - not distributed via Google Play, so Android will ask you to allow installing from this source the first time

Get it from the new Mobile App docs page - full feature list, download link, and setup steps.

v0.4.0 - SDK Security Hardening (all languages)

  • Anti-replay response verification across every SDK - Each server response is now cryptographically bound to the exact request that produced it, via a per-request nonce and HMAC. A captured response can no longer be replayed or spoofed through a fake/mock server, and the check now covers activation too, not just validation. Please update to the latest version: TypeScript 1.2.0, Python 1.2.0, Rust 1.1.0, C++ 2.1.0
  • C++ SDK v2.1.0 - TLS Certificate Pinning - Pins the exact API certificate, blocking man-in-the-middle proxies even when they use a locally-trusted root CA. Adds per-call-site keyed string obfuscation. Re-download astraguard.hpp from the SDK page and rebuild to get it
  • Python SDK v1.2.0 - Now connects reliably in production (fixed a CDN bot-protection block) and includes full nonce-based response verification on both validate and activate
  • Rust SDK v1.1.0 - Response signature verification is now fully functional and enforced on validate, activate and heartbeat

June 2026

v0.3.1 - Rust SDK & Python SDK

  • Rust SDK v1.0.2 live on crates.io - Full license validation with anti-debug, anti-VM, offline grace period, and heartbeat. AES-256-GCM offline response cache keeps your app running without internet. HWID fingerprinting and automatic heartbeat loop built in. Available in the SDK Downloads tab
  • Python SDK v1.1.0 live on PyPI - Both sync and async clients supported. AES-256-GCM encrypted local cache, HWID binding, heartbeat, and full validate/activate support. Available under Downloads & SDKs in your dashboard

v0.3.0 - Platform Improvements

  • Pricing change - The 30-day trial has been removed. Subscriptions now start immediately at $4.99/month
  • Login speed - Login is noticeably faster, especially on slower connections
  • Billing Portal - No more infinite loading when opening your Stripe subscription management from Settings
  • Newsletter setting - Your email notification preference now correctly saves in account settings
  • SDK tab fix - Version labels no longer show garbled characters

v0.2.9 - OAuth, Geo-Blocking & Key Forge Upgrades

  • Google & GitHub OAuth - Sign in with Google or GitHub instead of email and password. Existing accounts are automatically linked by email on first OAuth login. Available on both the registration and login pages
  • Custom Domain - All Plans - Custom Domain configuration is now available on all plans, including the Starter tier
  • Geo-Blocking - Block activations from specific countries per product. Configure a country blocklist in Product Security settings - any activation attempt from a blocked country is rejected immediately
  • IP Whitelist - Restrict activations to a list of trusted IP addresses per product. Requests from any IP not on the whitelist are blocked at the point of activation
  • Bundle Keys - Package multiple license keys together into a single distributable bundle. The entire bundle activates and validates as a unit, with all included keys tracked together
  • Trial Duration in Minutes & Seconds - Set trial key duration in minutes or seconds rather than days only. Useful for short demo windows and time-limited product previews
  • Max Activations per Key - Set a maximum activation count directly in the Forge Keys modal. Once the limit is reached the key stops accepting new activations automatically

May 2026

v0.2.8 - Reseller Security & Key Management

  • Deactivated Reseller Lockout - Deactivating a reseller now takes effect immediately. The account is blocked at login (no new token issued) and every API request returns 403 account_deactivated - even for sessions that were already active before deactivation
  • Reseller Key Actions - Full Developer Parity - Resellers now have the same key actions as developers: Freeze, Unfreeze, Revoke, Restore, and Delete - all with 2-click inline confirmation to prevent accidental changes
  • Trial & Subscription Key Expiry Fix - Trial and subscription keys generated by resellers no longer show as expired before first use. The expiry timer now correctly starts at first activation, not at key creation time
  • Reseller Origin in License Table - Reseller-generated keys now show a purple "via Reseller" badge + the reseller's company name in the User column so developers can instantly tell who generated each key
  • HWID Reset Reason - The reason a customer submitted with their HWID reset request is now visible to the developer in the HWID Reset panel
  • Confirm Dialog - All destructive actions now use a premium in-app confirmation dialog instead of the browser's native popup
  • File Manager Fixes - Files appear immediately after upload and no longer disappear when switching dashboard tabs
  • Product Image Editor - Edit your product's cover image directly from Product Settings: paste a URL or upload a file with live preview
  • Accessibility - ARIA roles on all tab panels, visible keyboard focus rings, accessible modal close buttons across the dashboard

v0.2.7 - Advanced Webhooks & API Keys

  • Webhooks: Custom Headers - Attach your own HTTP headers to every delivery, e.g. for server-side authentication on your endpoint
  • Webhooks: Auto-Retry - Failed deliveries are retried automatically with exponential backoff: 1 min → 5 min → 30 min → 2 h. No manual intervention needed
  • Webhooks: 3 New Events - license.validated, license.frozen, license.unfrozen are now available as subscribable events
  • Webhooks: Health Badge - Each webhook now shows a live health status and success rate badge on the dashboard: Healthy, Degraded, or Unhealthy
  • API Keys: Expiry Date - Create API keys with a fixed expiry date - the key auto-expires and stops working when the date is reached
  • API Keys: IP Restriction - Restrict an API key to a list of allowed IP addresses. Requests from any other IP are rejected with 403 ip_not_allowed
  • API Keys: Key Rotation - Rotate any API key with one click - same scopes, same settings, new secret. Ideal for regular security hygiene
  • API Keys: Last Used IP & Expiry Badge - API key cards now display the last IP that used the key and a colored expiry badge

v0.2.6 - Launcher & Auto-Delete

  • Launcher - Customers can launch your app directly from the Customer Portal. Binary is AES-256-GCM encrypted, decrypted in memory, never written to disk. HWID-bound & one-time token protected. MAX plan only
  • HWID-Bound Portal Access - Once a license key is activated on a machine, only that exact machine can log into the Customer Portal
  • Auto-Delete Expired Keys - New Key Lifecycle panel in Product Settings. Toggle auto-cleanup per product - expired keys removed every 6 hours. Delete Now button for instant cleanup

v0.2.5 - Customer Portal Notifications

  • Customer Portal Notifications - Customers now have a notification bell 🔔 in their portal. Approved and rejected HWID reset requests delivered directly to the customer - persistent, per license key, with unread badge and mark-all-read
  • Notification panel - Animated dropdown, color-coded icons per type, relative timestamps, click to mark read

v0.2.4 - Tags & Key Table Improvements

  • Tags - Create, manage and assign tags to license keys. Filter your key inventory by tag in one click. Now available to every developer
  • Active key highlighting - Active keys have a green left border + subtle tint for at-a-glance overview
  • License Type badges - Colored badges per type: 🟣 Lifetime, 🟡 Trial, 🟢 Subscription

v0.2.3 - Developer Tools & Analytics

  • Variable Inline Edit - Edit remote variables directly in the product panel without opening a modal. Pencil to start, save/cancel in place, secrets stay masked while editing
  • Webhook Retry - Failed deliveries now have a one-click retry button directly in the delivery log
  • Fraud Notifications - Receive in-app notifications when a high or critical fraud event is detected on any of your license keys. No setup needed - notifications appear automatically
  • Geographic Analytics - New geo panel in Analytics showing your top 10 countries by activation count
  • Export Status Filter - Dedicated status dropdown inside the CSV export menu (All / Active / Unused / Revoked), completely independent of the table filter
  • Webhook Delivery Log redesign - Syntax-highlighted JSON payloads, relative timestamps, color-coded status pills, split Request / Response view
  • Webhook Modal - Event pills grouped by category (License / Key / Product / Security) with Select All / None shortcuts and spring animation
  • Products Panel - Ambient glows always visible at rest, richer gradients, cards lift on hover, pulsing create button
  • Hybrid Signature fix - The "Quantum-Safe" label was incorrect. The feature is Ed25519 + SHA3-512 hybrid signing - panel, badges and descriptions updated to reflect what actually runs

v0.2.2 - Fine-Grained API Key Scopes

  • Fine-Grained API Key Scopes - 7 individual scopes instead of read / read+write. Grant exactly what each key needs, nothing more: keys:read, keys:write, products:read, products:write, webhooks:read, webhooks:write, analytics:read. Existing keys keep working

v0.2.1 - Key Details Panel

  • Expanded Key Row - Click the arrow on any license key to reveal full key details inline, without leaving the page
  • Last Seen - Shows the exact timestamp of the most recent validation request for each key
  • Full HWID - View the complete Hardware ID currently bound to a key
  • IP Address - Inspect the IP address from the most recent validation or activation
  • Activation Count - Track how many times a key has been activated
  • Inline Notes - Add and edit notes directly on any key from the key list
  • Recent Activity - Live log of the latest validation and activation events per key
  • Quick Actions - Freeze, Reset HWID, Revoke, Ban HWID, or Copy the key with a single click - all from the expanded row, no page navigation needed

v0.2.0 - Platform Overhaul

  • Two-Factor Authentication (2FA) - TOTP-based 2FA with backup codes for all accounts
  • Encrypted Binary Delivery - AES-256-GCM in-memory decryption via C++ loader, zero plaintext on disk
  • Discord Bot - Account verification, plan role assignment, server management
  • Reseller System - Invite resellers, assign quotas, track key distribution
  • Quantum-Safe Signatures - Optional quantum-resistant response signing per product
  • File Distribution - Cloudflare R2 storage, versioned releases, customer portal downloads
  • Force Updates - minVersion enforcement on every validation call
  • Webhook Delivery Logs - Full history with status codes and response times
  • Customer Portal - Self-service HWID reset requests, license status, file downloads

April 2026

v0.1.8 - SDK Downloads & Encrypted Delivery

  • Encrypted Delivery - AES-256-GCM file encryption for File Manager uploads and versioned Releases. Customers need a valid license to decrypt - a direct R2 breach is useless without a key
  • Release Encryption Badge - Encrypted releases show a 🔐 ENCRYPTED badge in the Release Manager
  • SDK Downloads page - All official SDKs available directly in the dashboard under Content → SDKs
  • Drag & Drop Upload - Files and Releases now support drag & drop onto the upload zone
  • Blacklist Pagination - Blacklist table now shows 10 entries per page with full navigation

v0.1.7 - Key Time Extension & Design

  • Key Time Extension - Extend the expiry of license keys: single (clock icon per key), bulk (select → Extend), or all at once (Extend All in Key Vault header)
  • Pricing Modal redesigned - Premium look with ambient glow, animated badge, live crypto rates & modern feature grid
  • Key Status Labels corrected - Unused = generated (not yet activated), Active = activated & HWID bound (green with pulse)

v0.1.6 - Key Freeze

  • Key Freeze - Freeze individual license keys without revoking them. Frozen keys return ERR_KEY_FROZEN on validation. Unfreeze anytime
  • Hourly Trial Licenses - Set trial durations in hours (1-8760h) instead of just days
  • Minimum Version Enforcement - Force outdated loaders to update. Set a minimum version on your product - any loader below it is blocked on validate()
  • Expires column - Expired keys are highlighted in red in the key table
  • Integrity Check Hash - Enter your binary hash directly in the dashboard; SDK verifies it automatically on every launch

v0.1.x - Beta Launch

  • Core license engine - Activation, validation, HWID binding
  • Feature Flags & Remote Variables - Toggle features and push config without redeploying
  • Fraud Detection - Velocity rules, HWID-hopping detection, IP analysis
  • Webhooks - Event-driven notifications for all key lifecycle events
  • TypeScript SDK - @astraguard/sdk on npm
  • C++ Header - Drop-in AstraGuard.h with XOR obfuscation
  • HMAC-SHA256 Response Signing - Tamper-proof server responses

Troubleshooting

Solutions to the most common issues when integrating AstraGuard.

License Validation Issues

reason: "hwid_mismatch"

Cause: The HWID sent in the request does not match the one bound at activation.

  • Make sure you generate the HWID the same way on every launch (same algorithm, same data sources)
  • Check that your HWID is generated using the helper from AstraGuard.h - use the same method on every launch
  • If the customer changed their hardware, they need to request an HWID reset via the Customer Portal

reason: "invalid_license"

Cause: The key does not exist or belongs to a different product.

  • Verify the productId in your request matches the product the key was generated for
  • Double-check you're sending the Product ID, not your API key. Product IDs are found under Dashboard → Product → Settings; API keys start with ag_ and belong in Authorization headers for management API calls only, never in productId
  • Check for extra whitespace or newline characters in the key string

reason: "version_too_old"

Cause: Your client's version is below the product's minVersion.

  • Update your application to the latest version
  • If you are the developer and did not intend this, lower minVersion in Product Settings

Validation always returns valid: false but no reason

  • Check that you are sending Content-Type: application/json
  • Ensure the request body is valid JSON (no trailing commas, properly quoted strings)
  • Confirm the productId field is present in the body

Activation Issues

Key activates on first machine but not second

This is expected behaviour - keys are bound to the first HWID at activation. The customer must request an HWID reset through the Customer Portal to move to a new machine.

reason: "vm_detected"

Your product has blockVm: true. Disable it in Product Settings → Security if you want to allow VM activations (e.g. for testing).

Webhook Issues

Webhook not receiving events

  • Go to Dashboard → Webhooks → [Your Webhook] → Deliveries to see delivery attempts and response codes
  • Ensure your endpoint returns HTTP 200 within 10 seconds
  • Check that your server is not blocking POST requests or requiring authentication headers not present in the delivery
  • Use Send Test to trigger a test delivery and check your server logs

Signature verification failing

Verify the raw request body (not parsed JSON) against the X-AstraGuard-Signature header:

typescript
import crypto from 'crypto'

const sig = req.headers['x-astraguard-signature'] // "sha256=abc123..."
const expected = 'sha256=' + crypto
  .createHmac('sha256', YOUR_WEBHOOK_SECRET)
  .update(req.rawBody)   // raw buffer, not JSON.stringify(req.body)
  .digest('hex')

if (sig !== expected) throw new Error('Invalid signature')

SDK Issues

Module not found: @astraguard/sdk

bash
npm install @astraguard/sdk
# If using monorepo, ensure the package is installed in the correct workspace

TypeScript: Property X does not exist on type ValidateResult

Make sure you are on the latest SDK version - type definitions are updated with new response fields:

bash
npm update @astraguard/sdk

Still Stuck?

Open a ticket in the #open-ticket channel on Discord. Include:

  • Your product ID (from Dashboard → Product → Settings)
  • The full API response you received (including HTTP status code)
  • The SDK version or language you are using

Migration Guide

Moving to AstraGuard from another licensing solution? This guide covers the most common migrations and what to expect.

General Migration Steps

  1. Create your product in AstraGuard dashboard
  2. Import or regenerate your keys - see below for bulk import
  3. Update your client to call POST /validate instead of your old provider
  4. Run both in parallel during rollout - validate against AstraGuard and keep old system as fallback
  5. Cut over once you're confident - disable old provider

Importing Existing Keys

If you have existing license keys you want to keep, use the bulk import endpoint:

http
POST /products/:id/keys/import
json
{
  "keys": [
    { "key": "OLD-KEY-1234", "type": "perpetual" },
    { "key": "OLD-KEY-5678", "type": "subscription", "expiresAt": "2027-01-01" }
  ]
}

HWID binding resets on migration

Imported keys start as unused. Customers must activate on AstraGuard once to re-bind their HWID. Inform them of this before cutting over.

Migrating from Stripe Licensing

If you used Stripe to gate access with subscription checks:

  • Keep Stripe for payment processing - AstraGuard handles the license layer
  • On Stripe webhook invoice.paid, generate a key via AstraGuard API and email it to the customer
  • On customer.subscription.deleted, revoke the key via POST /products/:id/keys/:key/revoke
typescript
// Stripe webhook handler example
if (event.type === 'invoice.paid') {
  const email = event.data.object.customer_email
  // Generate key via AstraGuard API
  const res = await fetch(`${AG_API}/products/${PRODUCT_ID}/keys`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${AG_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ count: 1, type: 'subscription', trialDays: 30 })
  })
  const { keys } = await res.json()
  await sendEmail(email, keys[0].key)
}

Migrating from Cryptolens / SKGL

  • Export your existing keys from Cryptolens as CSV
  • Use the bulk import endpoint to recreate them in AstraGuard
  • Replace Key.Activate() calls with POST /activate
  • Replace Key.IsOnRightMachine() logic with AstraGuard's built-in HWID binding

Migrating from a Custom Solution

  • Map your database key statuses to AstraGuard states: active, expired, revoked
  • Use /products/:id/keys/import to seed all keys
  • Replace your validation endpoint calls with POST /validate
  • Use Feature Flags to replace any custom feature gating logic
  • Use Remote Variables to replace any config pushed from your server

Client Code Changes

The minimum change in your client is replacing your old validation call:

typescript
// Before (generic example)
const valid = await oldProvider.check(licenseKey)

// After - AstraGuard
import { createClient } from '@astraguard/sdk'
const guard = createClient({ apiUrl: 'https://api.astraguard.io', productId: PRODUCT_ID })
const result = await guard.validate(licenseKey, { hwid: getMachineId() })
if (!result.valid) { /* handle */ }

Need help migrating?

Open a ticket in #open-ticket on Discord. We can help with bulk imports and custom migration scenarios.

Full API Reference

Complete reference for all developer-accessible endpoints. Authentication endpoints are excluded.

Base URL: https://api.astraguard.io
Auth: include Authorization: Bearer <token> (JWT) or X-API-Key: ag_... (API key) header on all protected endpoints.

Authentication

POST/loginPublic

Authenticate with email + password. Returns a JWT or a 2FA challenge.

Request

json
{ "email": "dev@example.com", "password": "yourpassword" }

Response - success

json
{ "token": "eyJ..." }

Response - 2FA required

json
{ "requires2FA": true, "challengeToken": "eyJ..." }
POST/login/2faPublic

Complete login when 2FA is enabled. Send the challenge token + 6-digit TOTP code or backup code.

Request

json
{ "challengeToken": "eyJ...", "code": "123456" }

Response

json
{ "token": "eyJ..." }

License Operations (Public - used by customer software)

POST/activatePublic

First-time activation. Binds the license key to the customer's HWID.

Request

json
{
  "key": "VIZ-XXXX-XXXX-XXXX",
  "hwid": "DESKTOP-ABC_JohnDoe",
  "productId": "uuid",
  "version": "1.0.0"
}

Response

json
{
  "success": true,
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "status": "active",
    "expiresAt": null,
    "features": [{ "name": "feature_x", "enabled": true }],
    "variables": { "server_url": "https://..." }
  },
  "signature": "base64..."
}

HWID binding

Once a key is activated with an HWID, all future calls must use the same HWID. To change hardware, the customer must request an HWID reset.

POST/validatePublic

Validate an already-activated key on every subsequent launch.

Request

json
{
  "key": "VIZ-XXXX-XXXX-XXXX",
  "hwid": "DESKTOP-ABC_JohnDoe",
  "productId": "uuid",
  "version": "1.0.0"
}

Response - valid

json
{
  "valid": true,
  "expiresAt": "2027-01-01T00:00:00Z",
  "features": [{ "name": "feature_x", "enabled": true }],
  "variables": { "server_url": "https://..." }
}

Response - invalid

json
{ "valid": false, "reason": "version_too_old" }

Reason codes

ReasonMeaning
invalid_licenseKey not found
revokedKey has been revoked
expiredSubscription/trial expired
frozenTemporarily disabled
hwid_mismatchHWID does not match the bound hardware
hwid_bannedHWID is blacklisted
version_too_oldClient version below minVersion
vm_detectedVirtual machine blocked by product setting
debugger_detectedDebugger blocked by product setting

Products

GET/productsDeveloper

List all products owned by the authenticated developer.

json
[{ "id": "uuid", "name": "My Loader", "keyPrefix": "ML-", "createdAt": "..." }]
POST/productsDeveloper

Create a new product.

json
{ "name": "My Loader", "keyPrefix": "ML-", "imageUrl": "https://..." }
PUT/products/:idDeveloper

Update product settings including minVersion, blockVm, blockDebug, webhookUrl, webhookEvents.

DELETE/products/:idDeveloper

Delete a product and all its keys. Irreversible.

License Keys

GET/products/:id/keysDeveloper · Scope: keys:read

List keys for a product (paginated). Query params: page, limit, status (unused/active/expired/revoked).

POST/products/:id/keysDeveloper · Scope: keys:write

Generate license keys in bulk.

json
{
  "count": 10,
  "type": "perpetual",
  "trialDays": null,
  "prefix": "ML-"
}

Types: perpetual, subscription, trial

GET/products/:id/keys/statsDeveloper · Scope: analytics:read

Key counts by status: { total, unused, active, expired, revoked }

GET/products/:id/keys/exportDeveloper · Scope: keys:read

Download all keys as CSV. Supports ?status=unused filter.

POST/products/:id/keys/:key/revokeDeveloper · Scope: keys:write

Revoke a single key. Reversible with /unrevoke.

POST/products/:id/keys/batch/revokeDeveloper · Scope: keys:write

Revoke multiple keys in one request. Also available: /batch/freeze, /batch/delete.

json
{ "keys": ["VIZ-AAAA-AAAA", "VIZ-BBBB-BBBB"] }

Features & Variables

GET/products/:id/featuresDeveloper · Scope: products:read

List feature flags for a product. Feature flags are returned in /validate responses so your software can gate functionality dynamically.

POST/features/:id/toggleDeveloper · Scope: products:write

Toggle a feature flag on or off. Changes take effect on the customer's next /validate call.

GET/products/:id/variablesDeveloper · Scope: products:read

List remote variables. Secret variables (isSecret: true) are never sent to customers - only shown in the dashboard.

Webhooks

GET/webhooksDeveloper · Scope: webhooks:read

List configured webhooks.

POST/webhooksDeveloper · Scope: webhooks:write

Create a webhook endpoint.

json
{
  "url": "https://yourdomain.com/webhook",
  "events": ["license.activated", "fraud.detected"],
  "secret": "your-webhook-secret"
}
POST/webhooks/:id/testDeveloper · Scope: webhooks:write

Send a test payload to verify your endpoint is receiving events correctly.

GET/webhooks/:id/deliveriesDeveloper · Scope: webhooks:read

View delivery history: status codes, response times, payloads sent.

Files & Releases

POST/upload/releaseDeveloper

Upload a file to Cloudflare R2 (max 100 MB). Send as multipart/form-data with field name file. Allowed extensions: .exe .dll .zip .rar .7z .msi.

GET/products/:id/check-updatePublic

Check if a newer release is available. Send current version as query param: ?version=1.0.0

json
{
  "hasUpdate": true,
  "latestVersion": "1.2.0",
  "downloadUrl": "https://...",
  "changelog": "Bug fixes and improvements"
}

API Keys

GET/user/api-keysDeveloper

List your API keys. Raw key values are never returned - only a masked preview.

POST/user/api-keysDeveloper

Create a new API key. The raw key is returned once - copy it immediately.

Request

json
{
  "name": "CI/CD Pipeline",
  "scopes": ["keys:read", "keys:write", "products:read"]
}

Response

json
{
  "id": "uuid",
  "name": "CI/CD Pipeline",
  "key": "ag_a1b2c3d4...",
  "scopes": ["keys:read", "keys:write", "products:read"],
  "createdAt": "2026-05-10T12:00:00Z"
}
DELETE/user/api-keys/:idDeveloper

Revoke an API key immediately. All requests using that key will fail with 401.

Webhook Events

Full payload reference for all 15 webhook events, plus signature verification code.

Every webhook POST includes a X-AstraGuard-Signature header: sha256=HMAC(secret, body). Always verify this before processing the payload.

Signature Verification

javascript
// Node.js / Express
const crypto = require('crypto')

function verifySignature(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))
}

// Express: use express.raw() to preserve raw body for verification
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-astraguard-signature']
  if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature')
  }
  const { event, ...data } = JSON.parse(req.body)
  // handle event...
  res.status(200).send('ok')
})
python
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET']

def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    sig = request.headers.get('X-AstraGuard-Signature', '')
    if not verify_signature(request.get_data(), sig, WEBHOOK_SECRET):
        abort(401)
    payload = request.json
    event = payload.get('event')
    # handle event...
    return '', 200
csharp
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;

[ApiController]
public class WebhookController : ControllerBase
{
    private readonly string _secret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET")!;

    [HttpPost("/webhook")]
    public IActionResult Handle()
    {
        using var reader = new StreamReader(Request.Body);
        var rawBody = reader.ReadToEndAsync().Result;

        var sig = Request.Headers["X-AstraGuard-Signature"].ToString();
        if (!VerifySignature(rawBody, sig))
            return Unauthorized();

        // parse and handle event
        return Ok();
    }

    private bool VerifySignature(string body, string signature)
    {
        var key = Encoding.UTF8.GetBytes(_secret);
        var data = Encoding.UTF8.GetBytes(body);
        using var hmac = new HMACSHA256(key);
        var hash = hmac.ComputeHash(data);
        var expected = "sha256=" + Convert.ToHexString(hash).ToLower();
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(signature));
    }
}

Event Payloads

license.created

Fired when a new license key is generated (single or bulk). Use this to sync your own database or send a welcome message to the key holder.

json
{
  "event": "license.created",
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "productId": "uuid",
    "type": "perpetual",
    "expiresAt": null,
    "createdAt": "2026-05-10T14:00:00Z"
  }
}

license.activated

Fired the first time a key is successfully activated and bound to an HWID.

json
{
  "event": "license.activated",
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "hwid": "DESKTOP-ABC_JohnDoe",
    "productId": "uuid",
    "activatedAt": "2026-05-10T14:30:00Z",
    "type": "perpetual",
    "expiresAt": null
  }
}

license.validated

Fired on every successful POST /validate call.

json
{
  "event": "license.validated",
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "hwid": "DESKTOP-ABC_JohnDoe",
    "productId": "uuid",
    "validatedAt": "2026-05-10T14:31:00Z"
  },
  "valid": true
}

license.revoked

json
{
  "event": "license.revoked",
  "license": { "key": "VIZ-XXXX-XXXX-XXXX", "productId": "uuid" },
  "revokedAt": "2026-05-10T15:00:00Z"
}

license.expired

json
{
  "event": "license.expired",
  "license": { "key": "VIZ-XXXX-XXXX-XXXX", "productId": "uuid", "expiredAt": "2026-05-10T00:00:00Z" }
}

hwid.reset

Fired when a customer submits an HWID reset request. The request is pending approval at this point.

json
{
  "event": "hwid.reset",
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "productId": "uuid",
    "oldHwid": "DESKTOP-ABC_JohnDoe"
  },
  "requestedAt": "2026-05-10T15:00:00Z"
}

license.frozen / license.unfrozen

json
{
  "event": "license.frozen",
  "license": { "key": "VIZ-XXXX-XXXX-XXXX", "productId": "uuid" }
}

key.generated

Fired after a bulk key generation completes. Useful for audit logging or notifying an internal system when new keys are available.

json
{
  "event": "key.generated",
  "productId": "uuid",
  "count": 50,
  "type": "perpetual",
  "generatedAt": "2026-05-10T16:00:00Z"
}

key.used

Fired on every activation or validation call - a low-level usage event. Subscribe to this only if you need per-request telemetry; for high-volume products this fires on every launch.

json
{
  "event": "key.used",
  "license": {
    "key": "VIZ-XXXX-XXXX-XXXX",
    "hwid": "DESKTOP-ABC_JohnDoe",
    "productId": "uuid"
  },
  "action": "validate",
  "valid": true,
  "timestamp": "2026-05-10T16:05:00Z"
}

product.created

Fired when you create a new product. Useful for CI/CD pipelines that need to react to product provisioning.

json
{
  "event": "product.created",
  "product": {
    "id": "uuid",
    "name": "My Loader v2",
    "keyPrefix": "LDR",
    "createdAt": "2026-05-10T09:00:00Z"
  }
}

product.updated

Fired when product settings are changed (name, webhook URL, security settings, min version, etc.).

json
{
  "event": "product.updated",
  "product": {
    "id": "uuid",
    "name": "My Loader v2",
    "updatedAt": "2026-05-10T12:00:00Z"
  },
  "changes": ["minVersion", "blockDebug"]
}

product.deleted

Fired when a product is permanently deleted. All associated keys become invalid after this event.

json
{
  "event": "product.deleted",
  "product": {
    "id": "uuid",
    "name": "My Loader v2"
  },
  "deletedAt": "2026-05-10T18:00:00Z"
}

fraud.detected

Fired when the fraud detection engine identifies suspicious activity on one of your keys.

json
{
  "event": "fraud.detected",
  "alert": {
    "type": "FRAUD_RULE_VIOLATION",
    "severity": "HIGH",
    "ip": "1.2.3.4",
    "hwid": "DESKTOP-ABC_JohnDoe",
    "keyId": "VIZ-XXXX-XXXX-XXXX",
    "productId": "uuid",
    "detectedAt": "2026-05-10T14:35:00Z"
  }
}

hwid.mismatch

Fired when a validation attempt uses the wrong HWID for an already-bound key.

json
{
  "event": "hwid.mismatch",
  "ip": "1.2.3.4",
  "hwid": "DIFFERENT-MACHINE_User",
  "keyId": "VIZ-XXXX-XXXX-XXXX",
  "productId": "uuid",
  "timestamp": "2026-05-10T14:36:00Z"
}

Subscribing to Events

When creating a webhook, pass the event names you want to receive. Use GET /webhooks-events to get the current full list.

json
{
  "url": "https://yourdomain.com/hooks/astraguard",
  "events": [
    "license.activated",
    "license.revoked",
    "hwid.reset",
    "fraud.detected",
    "hwid.mismatch"
  ],
  "secret": "your-secret-here"
}
Recommended starting set: license.activated, license.revoked, fraud.detected, hwid.mismatch. Subscribe to key.used or license.validated only if you need per-request telemetry - these fire on every launch and can be high volume.

Or set webhook events directly on a product via PUT /products/:id with webhookUrl and webhookEvents.

C++ / Game Loader Integration

Step-by-step guide for integrating AstraGuard into C++ game loaders, cheats, and desktop applications.

AstraGuard ships a ready-to-use C++ header (AstraGuard.h). Download it from Dashboard → Product → Dev Tools → SDK Download.

1. HWID Construction

The HWID uniquely identifies the customer's machine. Use the HWID helper provided by AstraGuard.h:

cpp
// Use the HWID helper from AstraGuard.h - implementation is intentionally not documented
std::string hwid = ag.getHWID();

2. Obfuscating Your API Key

Never store your API key as a plain string - it will be extracted by strings.exe. XOR-obfuscate it at compile time and deobfuscate at runtime:

cpp
// Store XOR-obfuscated key (XOR key defined in AstraGuard.h)
// # Use the XOR helper included with AstraGuard.h
static const uint8_t OBFUSCATED_KEY[] = { 0x1E, 0x19, 0x51, /* ... */ };
static const size_t KEY_LEN = sizeof(OBFUSCATED_KEY);

std::string DeobfuscateKey() {
    std::string key(KEY_LEN, '\0');
    for (size_t i = 0; i < KEY_LEN; i++)
        key[i] = OBFUSCATED_KEY[i] ^ XOR_KEY;  // XOR_KEY defined in AstraGuard.h
    return key;
}

3. License Validation

cpp
#include "AstraGuard.h"

int main() {
    std::string hwid = GetHWID();
    std::string userKey = GetLicenseKeyFromUser(); // your UI

    AstraGuard::Client client(
        "https://api.astraguard.io",
        "your-product-uuid",
        DeobfuscateKey()
    );

    // First run: activate (binds HWID)
    auto result = client.activate(userKey, hwid, "1.0.0");
    if (!result.success) {
        ShowError("Activation failed: " + result.reason);
        return 1;
    }

    // Subsequent runs: validate
    auto validation = client.validate(userKey, hwid, "1.0.0");
    if (!validation.valid) {
        if (validation.reason == "version_too_old") {
            ShowError("Please update to the latest version.");
        } else {
            ShowError("License invalid: " + validation.reason);
        }
        return 1;
    }

    // Check a feature flag
    if (validation.hasFeature("aimbot")) {
        EnableAimbot();
    }

    StartLoader();
    return 0;
}

4. Force Update Check

cpp
// Optionally check for updates before validation
auto update = client.checkUpdate("1.0.0");
if (update.hasUpdate) {
    ShellExecuteA(NULL, "open", update.downloadUrl.c_str(), NULL, NULL, SW_SHOW);
    ExitProcess(0);
}

5. Error Reason Codes

The reason field tells you exactly why validation failed. Handle each case explicitly:

ReasonMeaningWhat to show the user
invalid_licenseKey does not exist"Invalid license key."
hwid_mismatchKey bound to a different machine"This key is registered to a different device."
expiredTrial or subscription ended"Your license has expired. Please renew."
revokedManually revoked by developer"This license has been revoked."
frozenTemporarily disabled"This license is temporarily suspended."
version_too_oldClient version below minVersion"Update required - please download the latest version."
hwid_bannedThis machine is blacklisted"This device has been blocked."
blocked_ipRequest IP is blacklisted"Access blocked from this network."

6. Fail-Closed Pattern

Always fail closed: if the request to AstraGuard fails for any reason (network error, timeout, unexpected response), deny access. Never grant access on error.

cpp
bool ValidateOrDie(const std::string& key, const std::string& hwid) {
    try {
        auto result = client.validate(key, hwid, APP_VERSION);
        if (result.valid) return true;

        // Handle specific reasons
        if (result.reason == "version_too_old") {
            MessageBoxA(NULL, "Update required. Opening download page...", "Update", MB_OK);
            ShellExecuteA(NULL, "open", "https://yourdomain.com/download", NULL, NULL, SW_SHOW);
        } else {
            std::string msg = "License error: " + result.reason;
            MessageBoxA(NULL, msg.c_str(), "License Error", MB_ICONERROR | MB_OK);
        }
        return false;
    } catch (const std::exception& e) {
        // Network error, timeout, or unexpected response - fail closed
        MessageBoxA(NULL, "Could not verify license. Check your internet connection.", "Error", MB_ICONERROR | MB_OK);
        return false;
    }
}

int main() {
    std::string hwid = ag.getHWID();
    std::string key  = GetKeyFromUser();

    if (!ValidateOrDie(key, hwid)) {
        ExitProcess(1); // always exit - never continue on validation failure
    }

    StartLoader();
    return 0;
}

Anti-reverse-engineering tips

  • Obfuscate your product ID and API key (XOR or custom encryption)
  • Enable blockDebug: true in your product settings to detect debuggers at runtime
  • Enable integrityCheck: true to detect binary tampering
  • Call ExitProcess() immediately on any validation failure - never return or continue
  • Compile with optimizations and strip debug symbols for release builds

Node.js / Electron Integration

Integrate AstraGuard license validation into a Node.js app or Electron desktop application.

1. Install the SDK

bash
npm install @astraguard/sdk

2. Basic License Check

typescript
import { createClient } from '@astraguard/sdk'

const guard = createClient({
  apiUrl: 'https://api.astraguard.io',
  productId: 'your-product-uuid',
})

const result = await guard.validate('VIZ-XXXX-XXXX-XXXX')
if (result.valid) {
  console.log('License valid, expires:', result.license?.expiresAt)
  console.log('Features:', result.features)
} else {
  console.error('License invalid:', result.reason)
  process.exit(1)
}

3. Electron Main Process

typescript
// main.ts
import { app, BrowserWindow, ipcMain } from 'electron'
import { createClient } from '@astraguard/sdk'
import os from 'os'

const guard = createClient({
  apiUrl: 'https://api.astraguard.io',
  productId: process.env.PRODUCT_ID!,
})

// Build HWID from machine info
function getHWID(): string {
  return `${os.hostname()}_${os.userInfo().username}`
}

ipcMain.handle('validate-license', async (_event, licenseKey: string) => {
  const hwid = getHWID()
  try {
    // Try validate first (key already activated)
    const result = await guard.validate(licenseKey, { hwid })
    return result
  } catch {
    // First run - activate
    return guard.activate(licenseKey, { hwid })
  }
})

4. Activation vs Validation

A key must be activated once to bind it to an HWID, then validated on every subsequent launch. The SDK handles this automatically when you call validate() - it activates on first use and validates on all future calls. But you can also call them explicitly:

typescript
import { createClient } from '@astraguard/sdk'
import os from 'os'

const guard = createClient({
  apiUrl: 'https://api.astraguard.io',
  productId: process.env.PRODUCT_ID!,
})

const hwid = `${os.hostname()}_${os.userInfo().username}`

// First run - activate (binds HWID to this machine)
const activation = await guard.activate(licenseKey, { hwid, version: '2.1.0' })
if (!activation.success) {
  console.error('Activation failed:', activation.reason)
  process.exit(1)
}

// All subsequent runs - validate
const result = await guard.validate(licenseKey, { hwid, version: '2.1.0' })
if (!result.valid) {
  console.error('Validation failed:', result.reason)
  process.exit(1)
}

5. Feature Flags

The validate response includes the feature flags you configured for this product. Gate functionality based on what the developer has enabled:

typescript
const result = await guard.validate(licenseKey, { hwid })
if (!result.valid) process.exit(1)

// result.features is an array of { name: string, enabled: boolean }
const hasFeature = (name: string) =>
  result.features?.some(f => f.name === name && f.enabled) ?? false

if (hasFeature('premium_export')) {
  enablePremiumExport()
}

if (hasFeature('api_access')) {
  startApiServer()
}

// Remote variables are also available (public ones only)
const serverRegion = result.variables?.server_region ?? 'eu-central'

6. Error Reason Handling

typescript
const REASON_MESSAGES: Record<string, string> = {
  invalid_license: 'Invalid license key.',
  hwid_mismatch: 'This key is registered to a different machine.',
  expired: 'Your license has expired.',
  revoked: 'This license has been revoked.',
  frozen: 'This license is temporarily suspended.',
  version_too_old: 'A newer version is required. Please update.',
  hwid_banned: 'This device has been blocked.',
  blocked_ip: 'Access is blocked from this network.',
}

const result = await guard.validate(licenseKey, { hwid, version: APP_VERSION })
if (!result.valid) {
  const msg = REASON_MESSAGES[result.reason ?? ''] ?? `License error: ${result.reason}`
  showErrorDialog(msg)

  if (result.reason === 'version_too_old') {
    openBrowser('https://yourdomain.com/download')
  }
  process.exit(1)
}

7. Offline Grace Period

typescript
// Cache last valid result for up to 24h (for offline use)
import { createClient } from '@astraguard/sdk'
import fs from 'fs'

const CACHE_PATH = './license-cache.json'
const GRACE_PERIOD_MS = 24 * 60 * 60 * 1000

async function validateWithGrace(licenseKey: string) {
  const guard = createClient({ apiUrl: 'https://api.astraguard.io', productId: 'uuid' })

  try {
    const result = await guard.validate(licenseKey)
    if (result.valid) {
      fs.writeFileSync(CACHE_PATH, JSON.stringify({ ...result, cachedAt: Date.now() }))
    }
    return result
  } catch {
    // Offline fallback
    if (fs.existsSync(CACHE_PATH)) {
      const cache = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'))
      if (Date.now() - cache.cachedAt < GRACE_PERIOD_MS) {
        return { ...cache, offlineMode: true }
      }
    }
    return { valid: false, reason: 'offline_expired' }
  }
}
Store your product ID in an environment variable or Electron's safeStorage, not hardcoded in source. On Electron, call validate() from the main process only - never expose it to the renderer process.

.NET / Unity Integration

Integrate AstraGuard into C# applications and Unity projects using HttpClient.

1. HWID Construction

csharp
using System;

public static string GetHWID()
{
    string machineName = Environment.MachineName;
    string userName = Environment.UserName;
    return $"{machineName}_{userName}";
}

2. Activation + Validation

Call ActivateAsync the first time a key is used (binds the HWID), then ValidateAsync on every subsequent launch. Persist a flag in user settings to know which flow to run.

csharp
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

public class AstraGuardClient
{
    private static readonly HttpClient _http = new()
    {
        BaseAddress = new Uri("https://api.astraguard.io"),
        Timeout = TimeSpan.FromSeconds(10)
    };
    private readonly string _productId;

    public AstraGuardClient(string productId) => _productId = productId;

    public async Task<LicenseResult> ActivateAsync(string key, string hwid, string version)
        => await PostAsync("/activate", new { key, hwid, productId = _productId, version });

    public async Task<LicenseResult> ValidateAsync(string key, string hwid, string version)
        => await PostAsync("/validate", new { key, hwid, productId = _productId, version });

    private async Task<LicenseResult> PostAsync(string endpoint, object body)
    {
        var json = JsonSerializer.Serialize(body);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _http.PostAsync(endpoint, content);
        var raw = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<LicenseResult>(raw,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
    }
}

public record LicenseResult(
    bool Valid,
    bool Success,       // returned by /activate
    string? Reason,
    string? ExpiresAt,
    Feature[]? Features
);

public record Feature(string Name, bool Enabled);

3. Error Reason Handling + Fail-Closed

Always fail closed - never continue on a failed validation, even if the network is unavailable.

csharp
private static readonly Dictionary<string, string> ReasonMessages = new()
{
    ["invalid_license"]  = "Invalid license key.",
    ["hwid_mismatch"]    = "This key is registered to a different machine.",
    ["expired"]          = "Your license has expired. Please renew.",
    ["revoked"]          = "This license has been revoked.",
    ["frozen"]           = "This license is temporarily suspended.",
    ["version_too_old"]  = "A newer version is required. Please update.",
    ["hwid_banned"]      = "This device has been blocked.",
    ["blocked_ip"]       = "Access is blocked from this network.",
};

public static async Task<bool> ValidateOrExitAsync(
    AstraGuardClient client, string key, string hwid, string version)
{
    try
    {
        var result = await client.ValidateAsync(key, hwid, version);
        if (result.Valid) return true;

        var msg = ReasonMessages.GetValueOrDefault(result.Reason ?? "", $"License error: {result.Reason}");
        MessageBox.Show(msg, "License Error", MessageBoxButtons.OK, MessageBoxIcon.Error);

        if (result.Reason == "version_too_old")
            System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
            {
                FileName = "https://yourdomain.com/download",
                UseShellExecute = true
            });

        Environment.Exit(1);
        return false;
    }
    catch (Exception ex)
    {
        // Network error - fail closed
        MessageBox.Show("Could not verify license. Check your internet connection.\n\n" + ex.Message,
            "Connection Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
        Environment.Exit(1);
        return false;
    }
}

4. Feature Flags

csharp
var result = await client.ValidateAsync(licenseKey, hwid, "1.0.0");
if (!result.Valid) { /* handled above */ }

bool HasFeature(string name) =>
    result.Features?.Any(f => f.Name == name && f.Enabled) ?? false;

if (HasFeature("premium_export"))
    EnablePremiumExport();

if (HasFeature("dark_theme"))
    ApplyDarkTheme();

5. Unity Integration

csharp
// LicenseManager.cs - attach to a persistent GameObject
using UnityEngine;
using System.Collections;
using UnityEngine.Networking;

public class LicenseManager : MonoBehaviour
{
    private const string API_URL = "https://api.astraguard.io";
    private const string PRODUCT_ID = "your-product-uuid";

    void Start()
    {
        string savedKey = PlayerPrefs.GetString("licenseKey", "");
        if (!string.IsNullOrEmpty(savedKey))
            StartCoroutine(ValidateLicense(savedKey));
        else
            ShowLicenseInput();
    }

    IEnumerator ValidateLicense(string key)
    {
        string hwid = SystemInfo.deviceUniqueIdentifier; // Unity built-in
        string json = $"{{\"key\":\"{key}\",\"hwid\":\"{hwid}\",\"productId\":\"{PRODUCT_ID}\",\"version\":\"1.0\"}}";
        var request = new UnityWebRequest($"{API_URL}/validate", "POST");
        request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
        request.downloadHandler = new DownloadHandlerBuffer();
        request.SetRequestHeader("Content-Type", "application/json");
        yield return request.SendWebRequest();

        if (request.result == UnityWebRequest.Result.Success)
        {
            var result = JsonUtility.FromJson<ValidateResponse>(request.downloadHandler.text);
            if (result.valid) OnLicenseValid();
            else OnLicenseInvalid(result.reason);
        }
        else OnNetworkError();
    }

    void OnLicenseValid() { /* start game */ }
    void OnLicenseInvalid(string reason) { Debug.LogWarning("License invalid: " + reason); }
    void OnNetworkError() { /* offline fallback */ }

    [System.Serializable] class ValidateResponse { public bool valid; public string reason; }
}
Unity security notes:
  • Never store the license key in PlayerPrefs as plain text on release builds - it is readable by the player. Encrypt it with SystemInfo.deviceUniqueIdentifier as the key.
  • Put validation in a persistent DontDestroyOnLoad GameObject that runs before any game logic.
  • Use Application.Quit() (not a return) on validation failure to fully terminate.

API Reference

Complete reference for all developer-accessible endpoints. Key endpoints include request body fields and example responses. For the public /activate and /validate endpoints used by customer software, see their dedicated pages.

Base URL: https://api.astraguard.io  ·  JWT auth: Authorization: Bearer <token>  ·  API key auth: X-API-Key: ag_...

Authentication

POST/loginPublic

Authenticate with email + password. Returns JWT or 2FA challenge token.

FieldTypeRequiredDescription
emailstringrequiredAccount email address
passwordstringrequiredAccount password

Response (no 2FA):

json
{ "token": "eyJhbGci..." }

Response (2FA required):

json
{ "requires2FA": true, "challengeToken": "eyJhbGci..." }
POST/registerPublic

Create a new developer account. Invite code required if BETA_MODE is enabled.

FieldTypeRequiredDescription
emailstringrequiredEmail address
passwordstringrequiredPassword (min 8 chars)
namestringoptionalDisplay name
inviteCodestringconditionalRequired in beta mode
json
{ "token": "eyJhbGci...", "user": { "id": "uuid", "email": "you@example.com", "role": "developer" } }
POST/login/2faPublic

Complete 2FA login using the challenge token from /login.

FieldTypeRequiredDescription
challengeTokenstringrequiredShort-lived token from /login
codestringrequired6-digit TOTP code or 8-char backup code
json
{ "token": "eyJhbGci..." }
POST/forgot-passwordPublic

Send password reset email to the account associated with the provided email.

POST/reset-passwordPublic

Reset password via token received in email. Body: { "token": "...", "password": "newpass" }.

User Account

GET/user/meAuth

Get authenticated user's full profile.

PUT/user/profileAuth

Update profile: name, avatar URL.

PUT/user/passwordAuth

Change password (requires current password).

GET/user/sessionsAuth

List all active sessions.

DELETE/user/sessions/:sessionIdAuth

Revoke a specific session.

GET/plan-limitsAuth

Get plan limits (max products, keys, etc.).

API Keys

GET/user/api-keysAuth

List your API keys. Raw key values are never returned - only the name, preview, scopes, and metadata.

json
[
  {
    "id":          "uuid",
    "name":        "CI Deploy Key",
    "keyPreview":  "ag_...abc1",
    "scopes":      ["keys:read", "keys:write", "products:read"],
    "createdAt":   "2026-05-01T10:00:00Z",
    "lastUsedAt":  "2026-05-22T14:30:00Z"
  }
]
POST/user/api-keysAuth

Create a new API key. The raw key string is returned once only - copy it before closing the dialog.

FieldTypeRequiredDescription
namestringrequiredDescriptive label for the key
scopesstring[]optionalDefaults to all 7 scopes. See API Keys page for scope list.
json
{
  "id":     "uuid",
  "key":    "ag_live_xxxxxxxxxxxxxxxxxxx",
  "name":   "CI Deploy Key",
  "scopes": ["keys:read", "keys:write", "products:read"]
}
DELETE/user/api-keys/:idAuth

Revoke an API key immediately. Any requests using it will receive 401 from this point on.

Products

GET/productsDeveloper

List all products you own.

json
[
  {
    "id":          "uuid",
    "name":        "My Loader",
    "description": "v2 of my software",
    "keyPrefix":   "VIZ",
    "status":      "active",
    "imageUrl":    "https://...",
    "minVersion":  "1.2.0",
    "blockVm":     true,
    "blockDebug":  false,
    "createdAt":   "2026-04-01T00:00:00Z"
  }
]
POST/productsDeveloper

Create a new product.

FieldTypeRequiredDescription
namestringrequiredProduct display name
descriptionstringoptionalShort description
keyPrefixstringoptionalE.g. VIZ → keys look like VIZ-XXXX-XXXX
imageUrlstringoptionalHTTPS URL for product icon
json
{ "id": "uuid", "name": "My Loader", "keyPrefix": "VIZ", "createdAt": "2026-05-23T..." }
PUT/products/:idDeveloper

Update product settings. All fields are optional - only send what you want to change.

FieldTypeDescription
namestringProduct display name
minVersionstringMinimum allowed client version (force update)
blockVmbooleanReject activation from virtual machines
blockDebugbooleanReject if debugger detected
webhookUrlstringHTTPS URL to receive webhook events
webhookEventsstring[]Events to fire (e.g. ["license.activated"])
imageUrlstring | nullProduct icon URL (null to remove)
DELETE/products/:idDeveloper

Permanently delete a product and all associated keys. Irreversible - customers lose access immediately.

License Keys

GET/products/:id/keysDeveloper

List keys for a product, paginated.

Query ParamDefaultDescription
page1Page number
limit50Keys per page (max 200)
statusallFilter: unused / active / expired / revoked
search-Search by key string or HWID
json
{
  "keys": [
    {
      "id":         "uuid",
      "key":        "VIZ-XXXX-YYYY-ZZZZ",
      "status":     "active",
      "hwid":       "DESKTOP-ABC_jdoe",
      "type":       "perpetual",
      "expiresAt":  null,
      "activatedAt":"2026-05-10T14:30:00Z",
      "createdAt":  "2026-05-01T00:00:00Z"
    }
  ],
  "total": 247,
  "page":  1,
  "limit": 50
}
POST/products/:id/keysDeveloper

Generate license keys in bulk.

FieldTypeRequiredDescription
countnumberrequiredHow many keys to generate (1-500)
typestringrequiredperpetual, subscription, or trial
trialDaysnumberconditionalDays until expiry (required for trial/subscription)
prefixstringoptionalOverride product key prefix for this batch
json
{
  "keys": ["VIZ-AAAA-BBBB-CCCC", "VIZ-DDDD-EEEE-FFFF"],
  "count": 2
}
GET/products/:id/keys/statsDeveloper

Aggregated key counts by status.

json
{
  "total":   500,
  "unused":  253,
  "active":  198,
  "expired":  22,
  "revoked":  27
}
GET/products/:id/keys/exportDeveloper

Download all keys as CSV. Optional query: ?status=unused to filter. Response is text/csv with Content-Disposition: attachment.

POST/products/:id/keys/:key/revokeDeveloper

Revoke a single key. Reversible with /unrevoke. Returns { "success": true }.

POST/products/:id/keys/:key/unrevokeDeveloper

Restore a revoked key to its previous status.

POST/products/:id/keys/batch/revokeDeveloper

Bulk operations on multiple keys. Also available: /batch/freeze, /batch/unfreeze, /batch/delete.

FieldTypeDescription
keysstring[]Array of key strings to operate on
json
{ "keys": ["VIZ-AAAA-BBBB-CCCC", "VIZ-DDDD-EEEE-FFFF"] }

Features & Variables

GET/products/:id/featuresDeveloper

List feature flags (returned in /validate responses).

POST/products/:id/featuresDeveloper

Add feature flag to product.

POST/features/:id/toggleDeveloper

Toggle feature on/off (takes effect on next /validate).

DELETE/features/:idDeveloper

Delete feature flag.

GET/products/:id/variablesDeveloper

List remote variables (secret ones hidden from customers).

POST/products/:id/variablesDeveloper

Add remote variable. Optional: isSecret=true.

DELETE/products/:id/variables/:vidDeveloper

Delete variable.

Files & Releases

POST/upload/releaseDeveloper

Upload file to R2 (max 100 MB). Multipart, field: file. Extensions: .exe .dll .zip .rar .7z .msi.

GET/products/:pid/releasesDeveloper

List releases for product.

GET/products/:pid/releases/latestPublic

Get latest release (version + changelog).

PUT/products/:pid/releases/:ridDeveloper

Update release: version, changelog, isPrerelease.

DELETE/products/:pid/releases/:ridDeveloper

Delete release.

GET/products/:pid/check-updatePublic

Check for updates. Query: ?version=1.0.0. Returns: hasUpdate, latestVersion, downloadUrl, changelog.

Webhooks

GET/webhooksDeveloper

List all configured webhooks for your account.

POST/webhooksDeveloper

Create a new webhook endpoint.

FieldTypeRequiredDescription
urlstringrequiredHTTPS endpoint to receive payloads
eventsstring[]requiredEvent names to subscribe to
secretstringoptionalUsed to sign payloads with HMAC-SHA256
json
{
  "id":     "uuid",
  "url":    "https://yourserver.com/hooks/astraguard",
  "events": ["license.activated", "fraud.detected"],
  "secret": "your-webhook-secret"
}
PUT/webhooks/:idDeveloper

Update webhook URL, events list, or secret. Send only fields to change.

DELETE/webhooks/:idDeveloper

Delete webhook. Returns { "success": true }.

POST/webhooks/:id/testDeveloper

Send a test payload to verify the endpoint is reachable and returns 2xx.

GET/webhooks/:id/deliveriesDeveloper

View delivery history - HTTP status, response time, and payload for each attempt.

json
[
  {
    "id":           "uuid",
    "event":        "license.activated",
    "status":       "success",
    "statusCode":   200,
    "durationMs":   142,
    "deliveredAt":  "2026-05-23T10:15:00Z"
  }
]

Public License Endpoints (used by customer software)

These endpoints are called directly by your customers' machines - not from your server. They require no auth header.

POST/activatePublic

First-time activation. Binds the license key to the customer's HWID. See the Activation page for full request/response details.

FieldRequiredDescription
keyrequiredLicense key string
hwidrecommendedHardware identifier
productIdrequiredProduct UUID
versionoptionalClient version string
POST/validatePublic

Validate on every subsequent launch. Returns valid, features, and remote variables. See the Validation page for full details.

json
// Success:
{ "valid": true, "expiresAt": "2027-01-15T00:00:00Z", "features": [...], "variables": {...} }

// Failure:
{ "valid": false, "reason": "hwid_mismatch" }
GET/healthPublic

Server health check. Returns { "status": "ok", "maintenance": false }.

GET/products/:pid/check-updatePublic

Check if a newer release exists. Query: ?version=1.0.0.

json
{
  "hasUpdate":     true,
  "latestVersion": "2.1.0",
  "downloadUrl":   "https://...",
  "changelog":     "Bug fixes and performance improvements"
}

Rate Limits

AstraGuard applies rate limits to protect platform stability and fairness. Limits vary by plan and endpoint type.

Why Rate Limits?

Rate limiting prevents abuse, protects against DDoS attacks, and ensures fair access for all developers. Public endpoints like /validate have stricter limits to prevent customer software from hammering the API.

Plan Limits (Authenticated Requests)

These limits apply to all authenticated API requests when using a valid JWT or API key:

PlanPer MinutePer Day
Starter (free)30 requests1,000 requests
Max600 requests100,000 requests

Public Endpoint Limits

These limits apply per IP address to protect from abuse. Public endpoints are stricter since they're used by customer software across many machines:

EndpointLimitWindow
/validate & /activate100 requests1 minute per IP
/login & /register10 requests15 minutes per IP
/check-update50 requests5 minutes per IP

Rate Limit Headers

Every response includes these headers. Use them to implement client-side throttling:

bash
RateLimit-Limit:     600              # Your limit this minute
RateLimit-Remaining: 587              # Requests left before hitting limit
RateLimit-Reset:     1713441600       # Unix timestamp (seconds) when limit resets

Handling 429 Too Many Requests

When you exceed your limit, the API returns HTTP 429 with error details:

json
{
  "error": "Too many requests",
  "retryAfter": 60
}

retryAfter tells you how many seconds to wait before retrying. Implement exponential backoff to avoid rate limit hammering:

typescript
async function fetchWithRetry(url: string, options?: RequestInit, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    const res = await fetch(url, options)

    if (res.status === 429) {
      const retryAfter = parseInt(res.headers.get('Retry-After') ?? '60') * 1000
      const backoff = Math.min(1000 * Math.pow(2, i), 30000)  // Exponential backoff
      const waitMs = Math.max(retryAfter, backoff)

      console.warn(`Rate limited. Waiting ${waitMs}ms before retry ${i + 1}/${maxRetries}`)
      await new Promise(r => setTimeout(r, waitMs))
      continue
    }

    if (!res.ok) throw new Error(`HTTP ${res.status}`)
    return res
  }
  throw new Error('Exceeded max retries')
}

Best Practices

  • Check remaining quota: Monitor RateLimit-Remaining and self-throttle before hitting the limit.
  • Batch operations: Use bulk endpoints like POST /products/:id/keys/batch/revoke instead of individual calls.
  • Cache responses: Cache product info, features, and variables to avoid redundant API calls.
  • Implement backoff: Use exponential backoff with jitter when retrying after 429.
  • Upgrade your plan: If consistently hitting limits, upgrade from Starter to Max plan for 20x higher quota.
  • Contact support: For legitimate high-volume use cases, reach out on Discord for rate limit exceptions.

Rate Limit Scenarios

Scenario 1: Generating 1,000 keys

Use POST /products/:id/keys with count: 100 in a loop. Ten requests at 30/min = ~20 seconds. Under limit for both plans.

Scenario 2: Validating customer licenses at scale

Each customer machine calls /validate once per session (typically every 30-60 min). Distributed across many IPs, so per-IP limit of 100/min is generous for individual machines.

Scenario 3: Getting hit by the Starter plan limit

If you're consistently exceeding 30 req/min, upgrade to Max: 600 req/min (20x increase). Cost-effective for growing usage.

Rate Limit Expiration

Limits reset on a sliding window basis:

  • Per-minute: Resets every 60 seconds (oldest request falls off)
  • Per-day: Resets at UTC midnight
  • Per-IP (public): Resets after the window closes