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.
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
| Role | Access |
|---|---|
developer | Products, keys, files, analytics, resellers |
reseller | Reseller dashboard - manage quotas and customer keys |
customer | Customer 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
/validateon 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
- Installation - Install the SDK
- Quickstart - Create product → generate key → validate in 5 min
- Validation - Integrate
POST /validatein your software - Response Key - Verify server responses to prevent MITM
- 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
npm install @astraguard/sdk
# or
yarn add @astraguard/sdk
# or
pnpm add @astraguard/sdkEnvironment Variables
ASTRAGUARD_API_URL=https://api.astraguard.io
ASTRAGUARD_PRODUCT_ID=your-product-uuidNever 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
npm install @astraguard/sdkStep 4 - Validate
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
{
"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:
| Field | Description |
|---|---|
name | Display name |
keyPrefix | Optional prefix - e.g. VIZ- → keys look like VIZ-XXXX-XXXX |
imageUrl | Product icon (HTTPS, max 500 chars) |
minVersion | Minimum client version - older versions are rejected |
webhookUrl | URL to POST events to |
Security Settings
| Setting | Effect |
|---|---|
blockVm | Reject activations from virtual machines |
blockDebug | Reject if a debugger is attached at activation time |
integrityCheck | Verify binary hash matches stored integrityHash |
quantumSafe | Use 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:
{ "valid": false, "reason": "version_too_old" }Your software should show an "update required" dialog and exit.
Plan Limits
| Plan | Products | Licenses | Files/Product | Releases/Product | API Calls/Day | HWID Resets/Month |
|---|---|---|---|---|---|---|
| Starter | 1 | 100 | 1 | 1 | 1,000 | 3 |
| Max | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited | Unlimited |
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
| Type | Expiry | Use Case |
|---|---|---|
perpetual | Never | One-time purchase, lifetime access |
subscription | Configurable (days) | Recurring monthly/yearly billing |
trial | 1-720 hours | Free trial, demo access |
Key Lifecycle
unused → active (first activation binds HWID)
↓
expired / revoked / banned / frozenKey States
| State | Description | Reversible |
|---|---|---|
| active | In use, HWID bound | - |
| unused | Generated but never activated | - |
| frozen | Temporarily disabled by developer | Yes - unfreeze |
| revoked | Deactivated by developer | Yes - unrevoke |
| banned | Banned for abuse | Permanent |
| expired | Past expires_at date | Extend 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.
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 BConfiguring Bundles
Bundles are managed in Product Settings → Bundle Keys in the dashboard, or via the API.
Add a bundle
POST /products/:primaryProductId/bundles
Authorization: Bearer <token>
Content-Type: application/json
{
"includedProductId": "uuid-of-product-to-bundle"
}List bundles
GET /products/:primaryProductId/bundles
Authorization: Bearer <token>
// Response
[
{ "id": "uuid-a", "name": "Module A" },
{ "id": "uuid-b", "name": "Module B" }
]Remove a bundle
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:
{
"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
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()// 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
/validatecall - 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:
{
"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:
{
"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:
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:
{
"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:
# 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.
| Scope | What it covers |
|---|---|
keys:read | List keys, get stats, export CSV |
keys:write | Generate, revoke, freeze, batch operations |
products:read | List products, features, variables |
products:write | Create/update products, manage features & variables |
webhooks:read | List webhooks, view delivery history |
webhooks:write | Create, update, test webhooks |
analytics:read | Key stats and activation data |
Error: Insufficient Scope
If a key tries to call an endpoint it doesn't have scope for, you'll receive:
{
"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:
# 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
/validateValidate a license key. Call on every application launch.
{
"key": "XXXX-XXXX-XXXX-XXXX",
"hwid": "DESKTOP-ABC123_jdoe",
"productId": "your-product-uuid",
"version": "1.2.0"
}Request Fields
| Field | Required | Description |
|---|---|---|
key | required | The license key string |
hwid | recommended | Hardware identifier for binding |
productId | required | UUID of the product |
version | optional | Client 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
{
"valid": true,
"expiresAt": "2027-01-15T00:00:00.000Z",
"features": [{ "name": "pro", "enabled": true }],
"variables": { "serverUrl": "https://prod.example.com" }
}Failure Response
{ "valid": false, "reason": "hwid_mismatch" }Failure Reasons
| Reason | Meaning |
|---|---|
invalid_license | Key does not exist |
license_expired | Past expiry date |
hwid_mismatch | HWID doesn't match bound HWID |
hwid_banned | HWID is on the blacklist |
license_revoked | Revoked by developer |
license_frozen | Temporarily frozen |
license_banned | Banned for abuse |
version_too_old | Below minVersion |
vm_detected | VM detected (blockVm enabled) |
debugger_detected | Debugger attached (blockDebug enabled) |
SDK Example
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
/activateFirst-time activation - binds HWID to the key. Only needed once per device.
{
"key": "XXXX-XXXX-XXXX-XXXX",
"hwid": "DESKTOP-ABC123_jdoe",
"productId": "your-product-uuid",
"version": "1.2.0"
}Activation vs Validation
| /activate | /validate | |
|---|---|---|
| When | First launch only | Every subsequent launch |
| HWID | Binds HWID to key | Verifies HWID matches |
| Status change | unused → active | None |
| After HWID reset | Accepts new HWID on next activation | Requires 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
#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
@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
npm install @astraguard/sdkimport { 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
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.
dotnet add package AstraGuard.SDKusing 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
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
pip install astraguardfrom 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)
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.
#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
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.
# Add to Cargo.toml
cargo add astraguard
# Also add tokio runtime
cargo add tokio --features fulluse 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)
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.
// 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
/products/:id/response-keyReturns the HMAC-SHA256 key for this product. Requires developer auth. Key is derived deterministically - never stored in the database.
{
"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
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
- Customer logs into the Customer Portal with their license key
- Goes to Hardware tab → Request HWID Reset
- Enters a reason (e.g. "New PC build")
- Request appears in the developer's dashboard under HWID Resets
- Developer approves → key set to
RESET-PENDING - Customer activates on new hardware → new HWID bound, reset finalized
Cooldown & Eligibility
{
"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
/hwid-resets/:id/approveApprove reset - sets key to RESET-PENDING.
/hwid-resets/:id/rejectReject reset - no change to the key.
/license/:keyId/reset-hwidForce-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
/products/:id/releases{
"version": "2.1.0",
"changelog": "Bug fixes and performance improvements.",
"is_prerelease": false,
"file_id": "uuid-of-uploaded-file"
}Checking for Updates
/products/:id/check-update?version=1.0.0{
"hasUpdate": true,
"latestVersion": "2.1.0",
"downloadUrl": "https://r2.astraguard.io/...",
"changelog": "Bug fixes and performance improvements."
}Auto-Update Pattern
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
/customer/auth{
"key": "XXXX-XXXX-XXXX-XXXX"
}Returns a 2-hour JWT for web portal access.
Available Endpoints
| Feature | Endpoint |
|---|---|
| License info & status | GET /customer/license |
| HWID reset eligibility | GET /customer/hwid/info |
| Request HWID reset | POST /customer/hwid/reset |
| Browse & download files | GET /customer/files |
| Release list | GET /customer/releases |
| Remote variables | GET /customer/variables |
| Announcements | GET /customer/announcements |
License Info Response
{
"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
- Customer installs the Launcher component - one time only
- Customer clicks Launch in their Customer Portal
- A short-lived secure token is generated server-side
- The Launcher fetches the encrypted binary from AstraGuard servers
- The binary is decrypted and executed entirely in memory
- 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
- Go to your product in the Dashboard → Security tab
- Scroll to the Launcher section and toggle it ON
- Set the App Display Name - shown on the Launch button in the portal
- Upload your
.exein File Manager - Copy the File ID from File Manager and paste it into the Payload File field
- 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
| Guarantee | Detail |
|---|---|
| Never written to disk | The protected binary is decrypted and executed entirely in memory |
| Short-lived tokens | Launch tokens expire after a very short window and are single-use |
| Encrypted in transit | Payload is encrypted end-to-end between server and Launcher |
| HWID-verified | Server validates the customer's hardware before streaming the payload |
| Anti-analysis | Launcher exits silently if a debugger or analysis tool is detected |
| Memory cleaned up | All 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
/webhooks{
"url": "https://yourserver.com/webhook",
"events": ["license.activated", "license.revoked", "fraud.detected"],
"secret": "your-webhook-secret"
}Available Events
| Event | Fired When |
|---|---|
license.created | Key generated (single or bulk) |
license.activated | First activation - HWID bound |
license.validated | Every successful validation |
license.revoked | Key revoked by developer |
license.expired | Key passes expiry date |
license.frozen | Key frozen |
license.unfrozen | Key unfrozen |
key.generated | Bulk key generation completed |
key.used | Key used (activation or validation) |
product.created | Product created |
product.updated | Product settings changed |
product.deleted | Product permanently deleted |
hwid.reset | HWID reset requested by customer |
fraud.detected | Fraud alert triggered |
hwid.mismatch | Wrong HWID used on validation |
Verifying Signatures
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:
[
{
"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
/announcements{
"title": "Scheduled Maintenance",
"content": "API down for 30 min on Sunday 2am UTC.",
"target": "my_customers",
"type": "warning"
}Target Options
| Target | Visible To |
|---|---|
all | Every customer on the platform |
customers | All customers (alias for all) |
my_customers | Only customers using your products |
Reading Announcements
/announcementsAuthList all announcements visible to the current user. Customers only see entries matching their product owner or target all.
Response
[
{
"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
/announcements/unreadAuthReturns the count of unread announcements for the current user.
{ "count": 2 }Mark as Read
/announcements/:id/readAuthMark a specific announcement as read. Sets readAt to current timestamp.
Announcement Types
| Type | Badge Color | Use Case |
|---|---|---|
info | Blue | General news, feature announcements |
warning | Amber | Scheduled downtime, deprecations |
success | Green | Resolved incidents, new releases |
danger | Red | Active incidents, critical security notices |
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
// 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 binaries4. 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
// 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
.envin.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
| Setting | What It Does |
|---|---|
| Block Debuggers | Returns debugger_detected if a debugger is attached at activation |
| Block VMs | Returns vm_detected if running inside a virtual machine |
| Integrity Check | Verifies 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
| Code | Meaning |
|---|---|
200 | Success - check valid field for license status |
400 | Bad Request - missing or invalid parameters |
401 | Unauthorized - missing or expired JWT/API key |
403 | Forbidden - insufficient permissions |
404 | Not Found - resource does not exist |
429 | Too Many Requests - rate limit exceeded |
500 | Internal Server Error |
503 | Service Unavailable - maintenance mode |
License Reason Codes
These are returned as { "valid": false, "reason": "..." } with HTTP 200 - the request succeeded, but the license check failed.
| Reason | HTTP | Cause | Suggested User Message |
|---|---|---|---|
invalid_license | 200 | Key does not exist | "Invalid license key" |
license_expired | 200 | Past expiry date | "Your license has expired" |
license_revoked | 200 | Revoked by developer | "License revoked - contact support" |
license_frozen | 200 | Temporarily disabled | "License temporarily suspended" |
license_banned | 200 | Banned for abuse | "License banned" |
hwid_mismatch | 200 | HWID doesn't match bound value | "Hardware mismatch - request HWID reset" |
hwid_banned | 200 | HWID is blacklisted | "This device is banned" |
version_too_old | 200 | Below minVersion | "Please update your software" |
vm_detected | 200 | Virtual machine detected | "VMs are not supported" |
debugger_detected | 200 | Debugger attached | "Debugger detected" |
ip_banned | 200 | IP is blacklisted | "Your IP is banned" |
product_not_found | 200 | Unknown product ID | "Unknown product" |
Authentication & Authorization Errors
| Error | HTTP | Cause |
|---|---|---|
unauthorized | 401 | Missing or expired JWT / API key |
invalid_token | 401 | Malformed or tampered token |
forbidden | 403 | Role doesn't have permission for this action |
insufficient_scope | 403 | API key is missing a required scope |
email_not_verified | 403 | Account email not yet verified |
Validation & Resource Errors
| Error | HTTP | Cause |
|---|---|---|
missing_field | 400 | Required body field not provided |
invalid_field | 400 | Field value failed validation (e.g. bad URL) |
not_found | 404 | Resource doesn't exist or you don't own it |
conflict | 409 | Duplicate resource (e.g. feature name already exists) |
plan_limit_reached | 403 | Current plan doesn't allow more of this resource |
Rate Limit Errors
| Error | HTTP | Cause |
|---|---|---|
too_many_requests | 429 | Per-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
// 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
| Plan | Per minute | Per day |
|---|---|---|
| Starter (free) | 30 requests | 1,000 requests |
| Max | 600 requests | 100,000 requests |
Public Endpoint Limits
| Endpoint | Limit | Window |
|---|---|---|
/validate, /activate | 100 requests | 1 minute per IP |
/login, /register | 10 requests | 15 minutes per IP |
Rate Limit Headers
Every response includes these headers - use them to self-throttle your integration:
RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 1713441600 # Unix timestamp (seconds)Handling 429 Too Many Requests
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.
| Item | Where to configure | Priority |
|---|---|---|
| HWID binding enabled | Send hwid in every validate/activate request | Critical |
| Response signature verification | Response Key page - verify X-AstraGuard-Signature | Critical |
| Credentials obfuscated in binary | Use AstraGuard.h or XOR-encode manually | Critical |
| No offline fallback | If API call fails → exit, never assume valid | Critical |
Block debugger (blockDebug) | Product Settings in dashboard | High |
Block VM (blockVm) | Product Settings in dashboard | High |
Integrity check (integrityCheck) | Product Settings → set integrityHash on each release | High |
| Periodic re-validation | Re-validate every 30-60 min while running | High |
Version enforcement (minVersion) | Product Settings → bump after each security fix | Medium |
| Webhooks for anomaly alerts | Webhooks page → subscribe to fraud.detected, hwid.mismatch | Medium |
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.
// 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.
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.
// 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.
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.
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:
| Flag | What it does |
|---|---|
blockDebug | Rejects activate/validate when a debugger is attached |
blockVm | Rejects activate/validate from virtual machine environments |
integrityCheck | Validates 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.
# 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.
{
"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.
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.
| Language | Recommended 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# / .NET | No 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
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 memoryEnabling Encrypted Delivery
- Go to Dashboard → Products → [Your Product] → Settings → Security
- Find the Encrypted Delivery panel and flip the toggle ON
- Re-upload your
.exe- the server encrypts it automatically at upload time - Download
ag_config.jsonandag_payload.encfrom the File Manager tab - Build the loader (see below) and rename it to your software's name
- 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
cd sdk/loader
mkdir build && cd build
cmake .. -G "MinGW Makefiles"
cmake --build . --config Release
# Output: build/AstraGuardLoader.exeMSVC Build
cd sdk/loader
mkdir build && cd build
cmake .. -G "Visual Studio 17 2022"
cmake --build . --config Release
# Output: build/Release/AstraGuardLoader.exeRename 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.
{
"apiUrl": "https://api.astraguard.io",
"productId": "your-product-uuid",
"fileId": "file-uuid",
"originalName":"YourSoftware.exe",
"saveLicense": true
}| Field | Description |
|---|---|
apiUrl | AstraGuard API base URL |
productId | Your product UUID (from dashboard) |
fileId | The specific file UUID to decrypt |
originalName | Used as the temp-file name when launching |
saveLicense | If 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.
POST /files/:fileId/decrypt-key// 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.encwithout 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:
// 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:
US United States
GB United Kingdom
DE Germany
FR France
CA Canada
AU Australia
JP Japan
NL NetherlandsHandling ERR_GEO_BLOCKED in Your Software
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
}// 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:
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:
{ "ipWhitelist": "" }Error Response
// 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:
{ "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
- Go to Dashboard → Products → [Your Product] → Settings
- Set the Min Version field (e.g.
2.0.0) - Save - takes effect immediately for all new validation requests
Sending Version in Your Client
TypeScript SDK
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)
// 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
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 Version | minVersion | Result |
|---|---|---|
2.1.0 | 2.0.0 | valid |
1.9.9 | 2.0.0 | version_too_old |
2.0.0 | 2.0.0 | valid |
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
Developer (you)
└── invites Resellers
└── Reseller has Quotas (per product + key type)
└── Reseller generates Keys from quota
└── Reseller assigns Keys to CustomersInviting a Reseller
Go to Dashboard → Resellers → Invite Reseller. There are two flows:
| Scenario | Flow |
|---|---|
| Person already has an AstraGuard account | Enter their email → they receive a request to accept in their dashboard |
| New person, no account yet | Enter 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.
| Field | Description |
|---|---|
| Product | Which of your products the quota applies to |
| License Type | perpetual, subscription, or trial |
| Allocated | Total keys the reseller may generate |
| Used | Keys 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)
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 customerAuthenticate 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
| Charge | Amount | When |
|---|---|---|
| Monthly subscription | $4.99 / month | Starts 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)
- Click Get Access on astraguard.io
- Select Pay with Card
- Complete Stripe checkout - your account is upgraded instantly
- Register with the same email used at checkout
Cryptocurrency
We accept BTC, ETH, and SOL.
- Click Get Access → Pay with Crypto
- Select your coin, send the exact amount to the displayed address
- Submit your transaction hash and email address
- 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:
POST /stripe/portal → returns { url } to Stripe billing portalOr 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
- Get your unique referral link from Dashboard → Settings → Referral
- Share it with other developers
- When a referred developer signs up and upgrades to the Max plan, you earn 1 free month of Max
- 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:
https://www.astraguard.io/register?ref=YOUR_CODEAnyone 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:
| Field | Description |
|---|---|
| Referral Code | Your unique referral identifier |
| Total Referrals | Number of developers who signed up via your link |
| Months Earned | Free months already applied to your plan |
| Months Pending | Confirmed referrals awaiting application |
API: Get Referral Stats
GET /referrals/me
Authorization: Bearer <token>{
"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.
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.
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
- Tap Download APK at the top of this page
- 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
- 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
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.
- Join the server
- Go to the #verify channel
- Run the command:
/verify email:you@example.com - 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
| Role | Who |
|---|---|
| Newbie | Everyone on join - limited access until verified |
| Starter | Verified AstraGuard account on the Starter plan |
| Max | Verified AstraGuard account on the Max plan |
| Member | Assigned 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.hppfrom 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.unfrozenare 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 -
minVersionenforcement 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_FROZENon 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/sdkon npm - C++ Header - Drop-in
AstraGuard.hwith 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
productIdin 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 inproductId - 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
minVersionin 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
productIdfield 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
200within 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:
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
npm install @astraguard/sdk
# If using monorepo, ensure the package is installed in the correct workspaceTypeScript: 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:
npm update @astraguard/sdkStill 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
- Create your product in AstraGuard dashboard
- Import or regenerate your keys - see below for bulk import
- Update your client to call
POST /validateinstead of your old provider - Run both in parallel during rollout - validate against AstraGuard and keep old system as fallback
- 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:
POST /products/:id/keys/import{
"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 viaPOST /products/:id/keys/:key/revoke
// 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 withPOST /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/importto 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:
// 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.
https://api.astraguard.ioAuth: include
Authorization: Bearer <token> (JWT) or X-API-Key: ag_... (API key) header on all protected endpoints.Authentication
/loginPublicAuthenticate with email + password. Returns a JWT or a 2FA challenge.
Request
{ "email": "dev@example.com", "password": "yourpassword" }Response - success
{ "token": "eyJ..." }Response - 2FA required
{ "requires2FA": true, "challengeToken": "eyJ..." }/login/2faPublicComplete login when 2FA is enabled. Send the challenge token + 6-digit TOTP code or backup code.
Request
{ "challengeToken": "eyJ...", "code": "123456" }Response
{ "token": "eyJ..." }License Operations (Public - used by customer software)
/activatePublicFirst-time activation. Binds the license key to the customer's HWID.
Request
{
"key": "VIZ-XXXX-XXXX-XXXX",
"hwid": "DESKTOP-ABC_JohnDoe",
"productId": "uuid",
"version": "1.0.0"
}Response
{
"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.
/validatePublicValidate an already-activated key on every subsequent launch.
Request
{
"key": "VIZ-XXXX-XXXX-XXXX",
"hwid": "DESKTOP-ABC_JohnDoe",
"productId": "uuid",
"version": "1.0.0"
}Response - valid
{
"valid": true,
"expiresAt": "2027-01-01T00:00:00Z",
"features": [{ "name": "feature_x", "enabled": true }],
"variables": { "server_url": "https://..." }
}Response - invalid
{ "valid": false, "reason": "version_too_old" }Reason codes
| Reason | Meaning |
|---|---|
invalid_license | Key not found |
revoked | Key has been revoked |
expired | Subscription/trial expired |
frozen | Temporarily disabled |
hwid_mismatch | HWID does not match the bound hardware |
hwid_banned | HWID is blacklisted |
version_too_old | Client version below minVersion |
vm_detected | Virtual machine blocked by product setting |
debugger_detected | Debugger blocked by product setting |
Products
/productsDeveloperList all products owned by the authenticated developer.
[{ "id": "uuid", "name": "My Loader", "keyPrefix": "ML-", "createdAt": "..." }]/productsDeveloperCreate a new product.
{ "name": "My Loader", "keyPrefix": "ML-", "imageUrl": "https://..." }/products/:idDeveloperUpdate product settings including minVersion, blockVm, blockDebug, webhookUrl, webhookEvents.
/products/:idDeveloperDelete a product and all its keys. Irreversible.
License Keys
/products/:id/keysDeveloper · Scope: keys:readList keys for a product (paginated). Query params: page, limit, status (unused/active/expired/revoked).
/products/:id/keysDeveloper · Scope: keys:writeGenerate license keys in bulk.
{
"count": 10,
"type": "perpetual",
"trialDays": null,
"prefix": "ML-"
}Types: perpetual, subscription, trial
/products/:id/keys/statsDeveloper · Scope: analytics:readKey counts by status: { total, unused, active, expired, revoked }
/products/:id/keys/exportDeveloper · Scope: keys:readDownload all keys as CSV. Supports ?status=unused filter.
/products/:id/keys/:key/revokeDeveloper · Scope: keys:writeRevoke a single key. Reversible with /unrevoke.
/products/:id/keys/batch/revokeDeveloper · Scope: keys:writeRevoke multiple keys in one request. Also available: /batch/freeze, /batch/delete.
{ "keys": ["VIZ-AAAA-AAAA", "VIZ-BBBB-BBBB"] }Features & Variables
/products/:id/featuresDeveloper · Scope: products:readList feature flags for a product. Feature flags are returned in /validate responses so your software can gate functionality dynamically.
/features/:id/toggleDeveloper · Scope: products:writeToggle a feature flag on or off. Changes take effect on the customer's next /validate call.
/products/:id/variablesDeveloper · Scope: products:readList remote variables. Secret variables (isSecret: true) are never sent to customers - only shown in the dashboard.
Webhooks
/webhooksDeveloper · Scope: webhooks:readList configured webhooks.
/webhooksDeveloper · Scope: webhooks:writeCreate a webhook endpoint.
{
"url": "https://yourdomain.com/webhook",
"events": ["license.activated", "fraud.detected"],
"secret": "your-webhook-secret"
}/webhooks/:id/testDeveloper · Scope: webhooks:writeSend a test payload to verify your endpoint is receiving events correctly.
/webhooks/:id/deliveriesDeveloper · Scope: webhooks:readView delivery history: status codes, response times, payloads sent.
Files & Releases
/upload/releaseDeveloperUpload 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.
/products/:id/check-updatePublicCheck if a newer release is available. Send current version as query param: ?version=1.0.0
{
"hasUpdate": true,
"latestVersion": "1.2.0",
"downloadUrl": "https://...",
"changelog": "Bug fixes and improvements"
}API Keys
/user/api-keysDeveloperList your API keys. Raw key values are never returned - only a masked preview.
/user/api-keysDeveloperCreate a new API key. The raw key is returned once - copy it immediately.
Request
{
"name": "CI/CD Pipeline",
"scopes": ["keys:read", "keys:write", "products:read"]
}Response
{
"id": "uuid",
"name": "CI/CD Pipeline",
"key": "ag_a1b2c3d4...",
"scopes": ["keys:read", "keys:write", "products:read"],
"createdAt": "2026-05-10T12:00:00Z"
}/user/api-keys/:idDeveloperRevoke 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.
X-AstraGuard-Signature header: sha256=HMAC(secret, body). Always verify this before processing the payload.Signature Verification
// 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')
})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 '', 200using 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.
{
"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.
{
"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.
{
"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
{
"event": "license.revoked",
"license": { "key": "VIZ-XXXX-XXXX-XXXX", "productId": "uuid" },
"revokedAt": "2026-05-10T15:00:00Z"
}license.expired
{
"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.
{
"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
{
"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.
{
"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.
{
"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.
{
"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.).
{
"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.
{
"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.
{
"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.
{
"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.
{
"url": "https://yourdomain.com/hooks/astraguard",
"events": [
"license.activated",
"license.revoked",
"hwid.reset",
"fraud.detected",
"hwid.mismatch"
],
"secret": "your-secret-here"
}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.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:
// 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:
// 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
#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
// 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:
| Reason | Meaning | What to show the user |
|---|---|---|
invalid_license | Key does not exist | "Invalid license key." |
hwid_mismatch | Key bound to a different machine | "This key is registered to a different device." |
expired | Trial or subscription ended | "Your license has expired. Please renew." |
revoked | Manually revoked by developer | "This license has been revoked." |
frozen | Temporarily disabled | "This license is temporarily suspended." |
version_too_old | Client version below minVersion | "Update required - please download the latest version." |
hwid_banned | This machine is blacklisted | "This device has been blocked." |
blocked_ip | Request 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.
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: truein your product settings to detect debuggers at runtime - Enable
integrityCheck: trueto 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
npm install @astraguard/sdk2. Basic License Check
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
// 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:
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:
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
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
// 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' }
}
}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
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.
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.
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
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
// 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; }
}- Never store the license key in
PlayerPrefsas plain text on release builds - it is readable by the player. Encrypt it withSystemInfo.deviceUniqueIdentifieras the key. - Put validation in a persistent
DontDestroyOnLoadGameObject 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.
https://api.astraguard.io · JWT auth: Authorization: Bearer <token> · API key auth: X-API-Key: ag_...Authentication
/loginPublicAuthenticate with email + password. Returns JWT or 2FA challenge token.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | Account email address |
password | string | required | Account password |
Response (no 2FA):
{ "token": "eyJhbGci..." }Response (2FA required):
{ "requires2FA": true, "challengeToken": "eyJhbGci..." }/registerPublicCreate a new developer account. Invite code required if BETA_MODE is enabled.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | Email address |
password | string | required | Password (min 8 chars) |
name | string | optional | Display name |
inviteCode | string | conditional | Required in beta mode |
{ "token": "eyJhbGci...", "user": { "id": "uuid", "email": "you@example.com", "role": "developer" } }/login/2faPublicComplete 2FA login using the challenge token from /login.
| Field | Type | Required | Description |
|---|---|---|---|
challengeToken | string | required | Short-lived token from /login |
code | string | required | 6-digit TOTP code or 8-char backup code |
{ "token": "eyJhbGci..." }/forgot-passwordPublicSend password reset email to the account associated with the provided email.
/reset-passwordPublicReset password via token received in email. Body: { "token": "...", "password": "newpass" }.
User Account
/user/meAuthGet authenticated user's full profile.
/user/profileAuthUpdate profile: name, avatar URL.
/user/passwordAuthChange password (requires current password).
/user/sessionsAuthList all active sessions.
/user/sessions/:sessionIdAuthRevoke a specific session.
/plan-limitsAuthGet plan limits (max products, keys, etc.).
API Keys
/user/api-keysAuthList your API keys. Raw key values are never returned - only the name, preview, scopes, and metadata.
[
{
"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"
}
]/user/api-keysAuthCreate a new API key. The raw key string is returned once only - copy it before closing the dialog.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required | Descriptive label for the key |
scopes | string[] | optional | Defaults to all 7 scopes. See API Keys page for scope list. |
{
"id": "uuid",
"key": "ag_live_xxxxxxxxxxxxxxxxxxx",
"name": "CI Deploy Key",
"scopes": ["keys:read", "keys:write", "products:read"]
}/user/api-keys/:idAuthRevoke an API key immediately. Any requests using it will receive 401 from this point on.
Products
/productsDeveloperList all products you own.
[
{
"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"
}
]/productsDeveloperCreate a new product.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | required | Product display name |
description | string | optional | Short description |
keyPrefix | string | optional | E.g. VIZ → keys look like VIZ-XXXX-XXXX |
imageUrl | string | optional | HTTPS URL for product icon |
{ "id": "uuid", "name": "My Loader", "keyPrefix": "VIZ", "createdAt": "2026-05-23T..." }/products/:idDeveloperUpdate product settings. All fields are optional - only send what you want to change.
| Field | Type | Description |
|---|---|---|
name | string | Product display name |
minVersion | string | Minimum allowed client version (force update) |
blockVm | boolean | Reject activation from virtual machines |
blockDebug | boolean | Reject if debugger detected |
webhookUrl | string | HTTPS URL to receive webhook events |
webhookEvents | string[] | Events to fire (e.g. ["license.activated"]) |
imageUrl | string | null | Product icon URL (null to remove) |
/products/:idDeveloperPermanently delete a product and all associated keys. Irreversible - customers lose access immediately.
License Keys
/products/:id/keysDeveloperList keys for a product, paginated.
| Query Param | Default | Description |
|---|---|---|
page | 1 | Page number |
limit | 50 | Keys per page (max 200) |
status | all | Filter: unused / active / expired / revoked |
search | - | Search by key string or HWID |
{
"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
}/products/:id/keysDeveloperGenerate license keys in bulk.
| Field | Type | Required | Description |
|---|---|---|---|
count | number | required | How many keys to generate (1-500) |
type | string | required | perpetual, subscription, or trial |
trialDays | number | conditional | Days until expiry (required for trial/subscription) |
prefix | string | optional | Override product key prefix for this batch |
{
"keys": ["VIZ-AAAA-BBBB-CCCC", "VIZ-DDDD-EEEE-FFFF"],
"count": 2
}/products/:id/keys/statsDeveloperAggregated key counts by status.
{
"total": 500,
"unused": 253,
"active": 198,
"expired": 22,
"revoked": 27
}/products/:id/keys/exportDeveloperDownload all keys as CSV. Optional query: ?status=unused to filter. Response is text/csv with Content-Disposition: attachment.
/products/:id/keys/:key/revokeDeveloperRevoke a single key. Reversible with /unrevoke. Returns { "success": true }.
/products/:id/keys/:key/unrevokeDeveloperRestore a revoked key to its previous status.
/products/:id/keys/batch/revokeDeveloperBulk operations on multiple keys. Also available: /batch/freeze, /batch/unfreeze, /batch/delete.
| Field | Type | Description |
|---|---|---|
keys | string[] | Array of key strings to operate on |
{ "keys": ["VIZ-AAAA-BBBB-CCCC", "VIZ-DDDD-EEEE-FFFF"] }Features & Variables
/products/:id/featuresDeveloperList feature flags (returned in /validate responses).
/products/:id/featuresDeveloperAdd feature flag to product.
/features/:id/toggleDeveloperToggle feature on/off (takes effect on next /validate).
/features/:idDeveloperDelete feature flag.
/products/:id/variablesDeveloperList remote variables (secret ones hidden from customers).
/products/:id/variablesDeveloperAdd remote variable. Optional: isSecret=true.
/products/:id/variables/:vidDeveloperDelete variable.
Files & Releases
/upload/releaseDeveloperUpload file to R2 (max 100 MB). Multipart, field: file. Extensions: .exe .dll .zip .rar .7z .msi.
/products/:pid/releasesDeveloperList releases for product.
/products/:pid/releases/latestPublicGet latest release (version + changelog).
/products/:pid/releases/:ridDeveloperUpdate release: version, changelog, isPrerelease.
/products/:pid/releases/:ridDeveloperDelete release.
/products/:pid/check-updatePublicCheck for updates. Query: ?version=1.0.0. Returns: hasUpdate, latestVersion, downloadUrl, changelog.
Webhooks
/webhooksDeveloperList all configured webhooks for your account.
/webhooksDeveloperCreate a new webhook endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
url | string | required | HTTPS endpoint to receive payloads |
events | string[] | required | Event names to subscribe to |
secret | string | optional | Used to sign payloads with HMAC-SHA256 |
{
"id": "uuid",
"url": "https://yourserver.com/hooks/astraguard",
"events": ["license.activated", "fraud.detected"],
"secret": "your-webhook-secret"
}/webhooks/:idDeveloperUpdate webhook URL, events list, or secret. Send only fields to change.
/webhooks/:idDeveloperDelete webhook. Returns { "success": true }.
/webhooks/:id/testDeveloperSend a test payload to verify the endpoint is reachable and returns 2xx.
/webhooks/:id/deliveriesDeveloperView delivery history - HTTP status, response time, and payload for each attempt.
[
{
"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.
/activatePublicFirst-time activation. Binds the license key to the customer's HWID. See the Activation page for full request/response details.
| Field | Required | Description |
|---|---|---|
key | required | License key string |
hwid | recommended | Hardware identifier |
productId | required | Product UUID |
version | optional | Client version string |
/validatePublicValidate on every subsequent launch. Returns valid, features, and remote variables. See the Validation page for full details.
// Success:
{ "valid": true, "expiresAt": "2027-01-15T00:00:00Z", "features": [...], "variables": {...} }
// Failure:
{ "valid": false, "reason": "hwid_mismatch" }/healthPublicServer health check. Returns { "status": "ok", "maintenance": false }.
/products/:pid/check-updatePublicCheck if a newer release exists. Query: ?version=1.0.0.
{
"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:
| Plan | Per Minute | Per Day |
|---|---|---|
| Starter (free) | 30 requests | 1,000 requests |
| Max | 600 requests | 100,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:
| Endpoint | Limit | Window |
|---|---|---|
/validate & /activate | 100 requests | 1 minute per IP |
/login & /register | 10 requests | 15 minutes per IP |
/check-update | 50 requests | 5 minutes per IP |
Rate Limit Headers
Every response includes these headers. Use them to implement client-side throttling:
RateLimit-Limit: 600 # Your limit this minute
RateLimit-Remaining: 587 # Requests left before hitting limit
RateLimit-Reset: 1713441600 # Unix timestamp (seconds) when limit resetsHandling 429 Too Many Requests
When you exceed your limit, the API returns HTTP 429 with error details:
{
"error": "Too many requests",
"retryAfter": 60
}retryAfter tells you how many seconds to wait before retrying. Implement exponential backoff to avoid rate limit hammering:
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-Remainingand self-throttle before hitting the limit. - Batch operations: Use bulk endpoints like
POST /products/:id/keys/batch/revokeinstead 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