Scroll
EvoraDocs
Get started
C++ SDK
REST API
Manage
Endpoint reference
All endpoints
GETPlan quota and current usage
GETList applicationsPOSTCreate an applicationGETGet an applicationPUTUpdate an applicationDELETEDelete an application
GETApplication statisticsGETExtended overviewGETLogin activity by dayGETUser growth over timeGETLicense status breakdownGETSession trends
GETList licensesPOSTGenerate licensesGETGet a licensePUTUpdate a licenseDELETEDelete a licensePOSTBan a licensePOSTUnban a licensePOSTReset a license HWIDPOSTFreeze a licensePOSTResume a frozen licensePOSTAdjust license expiry by a signed deltaPOSTBulk license actions
GETLook up a customer by usernameGETList customersPOSTCreate a customerGETGet a customerPUTUpdate a customerDELETEDelete a customerPOSTMint a password-reset tokenPOSTConsume a reset token and set the new passwordPOSTBan a customerPOSTUnban a customerPOSTReset a customer's HWIDGETRead a customer's two-factor stateDELETEReset a customer's two-factor authenticationPOSTReset a customer's device bindingPOSTMint a one-time SDK login token (panel SSO)POSTRedeem a license key on a customer's behalfPOSTBulk customer actions
POSTAuthenticate a customer by license keyPOSTAuthenticate a customer by username and password
GETList a customer's subscriptionsPOSTGrant a subscription directlyDELETERemove a subscriptionPOSTFreeze a subscriptionPOSTResume a frozen subscriptionPOSTExtend a subscription by days
GETList tiersPOSTCreate a tierPUTUpdate a tierDELETEDelete a tier
GETList app variablesPOSTCreate or update a variableDELETEDelete all app variablesGETGet a variablePUTUpdate a variableDELETEDelete a variableGETList every user variable in the applicationGETList one customer's variablesPOSTSet a customer variableDELETEDelete all of a customer's variablesDELETEDelete a customer variable
GETList webhooksPOSTCreate a webhookGETGet a webhookPUTUpdate a webhookDELETEDelete a webhookPOSTFire a test deliveryGETRead the event stream (catch-up)
GETList blacklist entriesPOSTAdd a blacklist entryDELETERemove a blacklist entryGETList whitelist entriesPOSTAdd a whitelist entryDELETERemove a whitelist entry
GETList live sessionsDELETEKill a sessionPOSTKill every live session
GETList logsGETLog statistics
GETList sellersPOSTCreate a sellerGETGet a sellerPUTUpdate a sellerDELETEDelete a sellerPOSTAdd seller balance
GETList clientsPOSTCreate a clientPUTUpdate a clientDELETEDelete a clientGETList a client's application accessPOSTGrant application accessDELETERevoke application access
GETList entitlementsPOSTCreate an entitlementPUTUpdate an entitlementDELETEDelete an entitlementGETList entitlements attached to a tierPOSTAttach an entitlement to a tierGETResolve a customer's entitlements
GETList geo rulesPOSTAdd a geo ruleDELETERemove a geo rulePUTEnable or disable geo restrictions
GETList active leasesDELETERevoke a leaseGETSeat usage for a customer

Get started

Introducing the Evorion documentation

Everything you need to integrate authentication, licensing, file delivery, and runtime protection into your native Windows application.

Quick Start

Wire up the client, run your first authenticated call, and see the SDK's automatic heartbeat and integrity checks in action.

Installation

Drop the single header + static lib into your project. Zero external dependencies, zero build system changes.

Developer Checklist

The end-to-end list of what to configure before shipping — auth mode, version policy, integrity, protection macros.

Auth Modes

License key, username / password, or both. Learn how the SDK enforces your app's chosen authentication surface.

Explore the reference

Guides and references for every part of the Evorion SDK.

Authentication

Login, Register, License, Heartbeat, session variables, and file downloads.

Security Notice

The hardening playbook — session-secret pattern, inline refactor, heartbeat.

Security

Anti-debug, binary integrity, hardware ID, transport modes, version management.

Code Protection

CFF, SEH, encryption, sealed values, dynamic API. Ship code that reads garbage.

Developer API

Automate app, user, license, seller, and webhook management from your backend.

Management

API keys, subscriptions, blacklist / whitelist, sessions, sellers, and utilities.

RequiresMSVC 2019+C++17Windows x64

Installation

  1. Download the SDK— Grab the latest release from the Evora dashboard. You'll get Evorion.h and Evorion.lib.
  2. Drop into your project — Place both files alongside your source. The header handles all library linking via #pragma comment(lib, ...).
  3. Get your credentials — In the dashboard, go to Settings → Credentials to find your App ID and Owner ID for the public SDK surface.
Don't hardcode credentials in plaintext. Use the SecureCredential system or the EVSK() macro to encrypt them at compile time.

Quick Start

The simplest integration. Everything runs automatically:

main.cpp
#include "Evorion.h"

int main() {
    evorion::Client client(
        "OWNER_ID",
        "APP_ID",
        "1.0",
        evorion::TransportMode::Http,
        true,   // auto_init - calls Init() in constructor
        30,     // heartbeat every 30s
        500,    // anti-debug scan every 500ms
        true    // exit on tamper detection
    );

    if (!client.Initialized()) {
        std::cerr << client.LastError() << "\n";
        return 1;
    }

    // Session active. Heartbeat & anti-debug running in background.
    auto r = client.Login("user", "pass");
    if (!r.ok()) return 1;

    std::cout << "Welcome, " << client.User().username << "\n";
    client.Wait();  // blocks forever, keeps heartbeat alive
}

Developer Checklist

What you actually need to do, end to end, before shipping.

  1. Create a developer account — sign up at the dashboard. Free tier (Core) is enough to evaluate; Pro adds server-decrypted sections and higher limits; Ultra adds attestation depth, seller tooling, and top quotas.
  2. Create an application — in the dashboard, add an app and copy the app_id, app_secret, and (optional) public key.
  3. Pick an authentication mode — license for license-key only, user_pass for username/password, or both. See Authentication Modes.
  4. Drop the SDK into your project — add the headers and link the static lib. All you need is #include <evorion/evorion.h>.
  5. Initialize the client — instantiate Evorion::Client with your app_id, app_secret, and version string. The constructor starts heartbeat and anti-debug automatically.
  6. Wire your auth flow — call Login(), Register(), or License() based on your chosen mode. Inspect the Result for the outcome.
  7. Configure version policy — set ok / warn / block rules per version in the dashboard so old clients can't bypass updates.
  8. Enable integrity checking — turn on Anti-Debug, Anti-VM, and Integrity for your app. The SDK will register a golden image on first connect.
  9. Wrap sensitive code with real protection primitives — EVORION_ENCRYPT_BEGIN/END for post-build code encryption (evora-protect CLI), EVORION_AUTH_PROTECT for auth-gated blocks, EVORION_LOCKED_INT for auth-gated constants, EVORION_DYNAPI to hide imports, EVSK() for compile-time string encryption, and the sscx function marker for MAX-tier server-side lift.
  10. Test in transport modes you'll use — HTTPS works everywhere; WebSocket gives lower latency and server push.
  11. Set up sessions and ban policies — pick concurrent session limits, ban triggers, and abuse detection thresholds in the dashboard.
  12. Optionally use the Developer API — issue scoped API keys and automate user, license, or seller management from your own backend.
  13. Ship it — release the build that uploads its own golden image, then push subsequent builds confidently knowing remote attestation will catch tampered binaries.

Common pitfalls

  • Forgetting to bump the version string when you push a new build — the server will reject the new client until a matching golden image is registered.
  • Hardcoding plaintext secrets — wrap every credential and API key with EVSK(). Plain string literals end up in your binary.
  • Using license auth mode and then calling Login() — the SDK will refuse the call. Check your app's auth mode in the dashboard.
  • Skipping Wait() in console apps — without it, the process exits and the heartbeat dies, which the server treats as session termination.
  • Disabling Anti-Debug during development — fine while you're stepping through your own code, but never ship a build with it off.

Authentication Modes

Each application has an auth mode set in the dashboard. The SDK reads it after Init() and enforces it on login calls.

ModeAllowedBlocked
bothLogin() · Register() · License()None
licenseLicense()Login() · Register()
user_passLogin() · Register()License()

Calling a blocked method returns ErrorCode::AuthModeRestricted. Check the mode first to show the right UI:

auth_routing.cpp
std::string mode = client.GetAuthMode();

if (mode == "license") {
    std::string key;
    std::cout << "License key: ";
    std::getline(std::cin, key);
    auto r = client.License(key);

} else if (mode == "user_pass") {
    std::string user, pass;
    std::cout << "Username: "; std::getline(std::cin, user);
    std::cout << "Password: "; std::getline(std::cin, pass);
    auto r = client.Login(user, pass);

} else {
    // "both" - let the user choose their flow
}

Login

Result Login(const std::string& username, const std::string& password, const std::string& totp_code = "")

Authenticates with username and password. Populates User() on success. Blocked when auth mode is "license".

Leave totp_codeempty on the first attempt. If the account has two-factor enabled, or the app's policy requires it, the call fails with code TWOFA_REQUIRED — prompt for the six-digit code and call again with it filled in. The server only reports this after the password verifies, so a wrong password never reveals whether the account exists.

cpp
auto r = client.Login("user", "pass");
if (!r.ok()) {
    std::cerr << r.message() << "\n";
    if (r.error_code == evorion::ErrorCode::InvalidCredentials)
        std::cerr << "wrong username or password\n";
}

Register

Result Register(const std::string& username, const std::string& password)

Creates a new user account. Does not auto-login, so call Login() afterwards. Blocked when auth mode is "license".

cpp
auto r = client.Register("newuser", "securepass");
if (r.ok()) {
    // now login with the new account
    auto login = client.Login("newuser", "securepass");
}

License Key

Result License(const std::string& license_key, const std::string& totp_code = "")

Validates and activates a license key. Populates User() on success. Blocked when auth mode is "user_pass".

Key-only auth runs the same second-factor check as Login(). A license key is a bearer credential — anyone holding the string can use it — so it is the case where a second factor earns its keep. Handle TWOFA_REQUIRED the same way.

cpp
auto r = client.License("XXXXX-XXXXX-XXXXX");
if (!r.ok()) {
    if (r.error_code == evorion::ErrorCode::InvalidLicense)
        std::cerr << "key not found or already used\n";
}

Client

The main SDK class. Non-copyable, non-assignable. Two constructor variants:

Plaintext constructor

cpp
evorion::Client(
    const std::string& owner_id,
    const std::string& app_id,
    const std::string& version,
    TransportMode mode          = TransportMode::Http,
    bool          auto_init     = true,
    int           heartbeat_interval = 30,
    int           antidebug_interval = 500,
    bool          auto_exit     = true
);

Secure constructor

Uses SecureCredential for a compile-time-encrypted owner id. Recommended for production builds.

cpp
evorion::Client(
    const SecureCredential& owner_id,
    const std::string&      app_id,
    const std::string&      version,
    TransportMode mode          = TransportMode::Http,
    bool          auto_init     = true,
    int           heartbeat_interval = 30,
    int           antidebug_interval = 500,
    bool          auto_exit     = true
);

Parameters

ParameterDescription
owner_idYour owner UUID from the dashboard
app_idApplication UUID
versionYour app version string (for version gating)
modeHttp or WebSocket. WebSocket enables server push.
auto_initCalls Init() automatically in the constructor
heartbeat_intervalHeartbeat interval in seconds. Set 0 to disable.
antidebug_intervalAnti-debug scan interval in milliseconds. Set 0 to disable.
auto_exitTerminate process on tamper detection

Utility methods

MethodReturnsDescription
Initialized()boolWhether Init() succeeded
Authenticated()boolWhether user is logged in
LastError()const string&Last error message
LastErrorCode()ErrorCodeLast typed error code
User()const UserData&Current user data (after auth)
IsBlacklisted()boolWhether this HWID is banned
GetAppName()stringApp name from server config
GetAuthMode()string"license", "user_pass", or "both"
GetSdkVersion()stringSDK version (e.g. "2.9.7")
IsWebSocketConnected()boolWS transport connection status
Wait()voidBlock forever (heartbeat stays alive)
Close()voidTears down the client, stops all timers and closes transports

Result

Returned by every API call. Check ok() first, then read details.

Field / MethodTypeDescription
ok()boolOperation succeeded
message()stringHuman-readable status or error
error_codeErrorCodeTyped enum for programmatic handling
errorstringRaw error string
jsonstringRaw JSON response body
blacklistedboolDevice/user is on the blacklist
FileContents()vector<uint8_t>Binary file data (only after DownloadFile())
FileName()stringOriginal filename (only after DownloadFile())
error handling pattern
auto r = client.Login("user", "pass");
if (!r.ok()) {
    std::cerr << "Error: " << r.message() << "\n";
    std::cerr << "Code:  " << (int)r.error_code << "\n";

    if (r.blacklisted)
        std::cerr << "device is banned\n";
}

UserData

Populated after successful Login() or License(). Access via client.User().

Fields

FieldTypeDescription
usernamestringAccount username
hwidstringHardware ID bound to this account
ipstringIP address (server-reported)
subscriptionstringSubscription plan name
subscription_levelintNumeric tier level
expirystringExpiry date/time
create_dateint64_tAccount creation (Unix timestamp)
last_loginint64_tLast login (Unix timestamp)
variablesmap<string,string>Server-defined per-user key-value data

Helper methods

MethodReturnsDescription
HasSubscription()boolHas any active subscription
IsLifetime()boolSubscription never expires
GetTimeLeftSeconds()int64_tSeconds until expiry
FormatTimeLeft()stringFormatted time remaining
GetVariable(key, default)stringLookup a user variable
IsValid()boolWhether UserData has been populated
cpp
auto& user = client.User();
std::cout << "User: " << user.username << "\n";
std::cout << "Plan: " << user.subscription << "\n";
std::cout << "Time: " << user.FormatTimeLeft() << "\n";

// read a custom variable set in the dashboard
std::string tier = user.GetVariable("tier", "free");

Error Codes

evorion::ErrorCode enum for programmatic error handling:

CodeValueMeaning
None0No error
Unknown1Unclassified
InvalidCredentials2Wrong username/password
InvalidLicense3License key not found or already used
HwidMismatch4Hardware doesn't match registered device
UserBanned5Account is banned
SubscriptionExpired6Subscription ran out
NoSubscription7No active subscription
DeviceMismatch8Device fingerprint mismatch
SessionExpired9Session token expired
IntegrityViolation10Binary tamper detected
VersionBlocked11App version is blocked
AuthModeRestricted12Auth mode doesn't allow this method
ServerError13Server-side error

Init

Result Init()

Establishes a session with the server: device registration, session tokens, and app config. Only needed when auto_init is false.

manual init
evorion::Client client(owner, app, ver,
    evorion::TransportMode::Http,
    false  // auto_init off
);

auto r = client.Init();
if (!r.ok()) {
    std::cerr << "init failed: " << r.message() << "\n";
    return 1;
}

Check

Result Check()

Validates the current session is still live on the server. Useful for manual session verification outside the automatic heartbeat cycle.

cpp
auto r = client.Check();
if (!r.ok()) {
    // session died, re-authenticate
}

Heartbeat

Result Heartbeat()

Keeps the session alive. Runs automatically in a background thread (controlled by heartbeat_sec), but you can call it manually if needed.


GetVar

Result GetVar(const std::string& var_name)

Fetches a server-side variable by name. Value is returned in Result::json.

cpp
auto r = client.GetVar("motd");
if (r.ok())
    std::cout << "Message: " << r.json << "\n";

SetUserVar New

Result SetUserVar(const std::string& key, const std::string& value)

Sets a per-user variable on the server. Each user has their own isolated key-value store scoped to your application. If the key already exists it will be overwritten, unless the variable was marked read-only by the developer in the dashboard, in which case the call fails gracefully. Keys can be up to 100 characters, values up to 10,000 characters.

Variables set from the SDK are read-write by default. Developers can lock specific variables to read-only from the dashboard.
cpp
// save a play count
auto r = client.SetUserVar("play_count", "42");
if (!r.ok())
    std::cerr << "set failed: " << r.message() << "\n";

// save a JSON config (values are strings)
client.SetUserVar("settings", R"({"fov":90,"sens":2.5})");

GetUserVar New

Result GetUserVar(const std::string& key = "")

Retrieves per-user variables. Pass a key to get a single variable, or call with no arguments to retrieve all variables for the current user as a JSON object.

single variable
auto r = client.GetUserVar("play_count");
if (r.ok())
    std::cout << "plays: " << r.json << "\n";
all variables
// empty key = get everything
auto all = client.GetUserVar();
if (all.ok())
    std::cout << "all vars: " << all.json << "\n";
// output: {"play_count":"42","settings":"{\"fov\":90}"}
GetVar vs GetUserVar. GetVar() fetches application-wide variables (same value for all users). GetUserVar() fetches per-user variables (unique to each authenticated user).

FetchOnline New

Result FetchOnline()

Returns the number of currently online users in your application. A user is considered online if they have an active session with a heartbeat within the last 5 minutes. The count is returned in Result::json as the online field.

cpp
auto r = client.FetchOnline();
if (r.ok())
    std::cout << "Users online: " << r.json << "\n";
// output: {"success":true,"online":17}

Ban New

Result Ban(const std::string& reason = "")

Permanently bans the currently authenticated user. The server kills the active session immediately after. This is irreversible from the SDK. Only a developer can unban the user from the dashboard. Useful as a client-side anti-tamper response.

Caution.This permanently bans the user's account. The session is terminated server-side and the ban reason is logged.
cpp
// detected something suspicious, ban and exit
client.Ban("Tamper detected by client");
std::exit(1);

InvokeWebhook NewPro / Ultra

Result InvokeWebhook(const std::string& webhook_id, const std::string& data_json = "")

Triggers a server-side webhook from the SDK. The webhook must be marked as SDK Callable in the developer dashboard. The webhook URL is never exposed to the client. Pass an optional JSON object as data_json to include custom data in the webhook payload. The server will POST to the configured URL with event details including user ID, IP, timestamp, and custom data.

Dashboard setup required.Go to your app's Webhooks page, create a webhook, and enable the SDK Callable toggle. If Authenticated Only is on (default), only logged-in users can invoke it.
fire with custom data
auto r = client.InvokeWebhook(
    "wh-uuid-from-dashboard",
    R"({"event":"level_complete","score":9500})"
);
if (!r.ok())
    std::cerr << "webhook failed: " << r.message() << "\n";
simple ping
client.InvokeWebhook("wh-uuid-from-dashboard");

DownloadFile New in v2.9.7

Result DownloadFile(const std::string& file_id)

Downloads a file from the server by its file ID. The file must be uploaded through the dashboard under your application. On success, use Result::FileContents() for the raw binary data and Result::FileName() for the original filename.

Files are delivered end-to-end encrypted. The server never sees plaintext file contents. Decryption happens client-side in the SDK.
download & save to disk
auto r = client.DownloadFile("file-uuid-from-dashboard");
if (!r.ok()) {
    std::cerr << "download failed: " << r.message() << "\n";
    return 1;
}

std::vector<uint8_t> data = r.FileContents();
std::string name = r.FileName();

std::ofstream out(name, std::ios::binary);
out.write(reinterpret_cast<const char*>(data.data()), data.size());
load into memory
auto r = client.DownloadFile("config-file-id");
if (r.ok()) {
    auto bytes = r.FileContents();
    std::string text(bytes.begin(), bytes.end());
    std::cout << "loaded " << bytes.size() << " bytes\n";
}

OnPush

void OnPush(std::function<void(const std::string& type, const std::string& payload)> cb)

Registers a callback for server-pushed messages. Only works in WebSocket transport mode. Common push types include "kill" and "ban".

cpp
client.OnPush([](const std::string& type, const std::string& payload) {
    if (type == "kill" || type == "ban") {
        std::cerr << "Terminated by server: " << payload << "\n";
        ExitProcess(0);
    }
});

Two-factor authentication

Your users can protect their account with any standard authenticator app — Google Authenticator, Authy, 1Password, Bitwarden. Evora implements TOTP (RFC 6238): six digits, thirty-second step, no third-party service in the path.

You choose the policy per application under Settings → Access policies → Two-factor authentication:

PolicyBehaviour
DisabledNobody can enrol. Existing enrolments are ignored at login.
OptionalUsers may enrol. Those who have are challenged; those who have not sign in normally. This is the default.
RequiredSign-in is refused with TWOFA_ENROLMENT_REQUIRED until the user enrols. Users cannot turn it back off.
Where the check sits. The second factor is verified after the password (or licence key) proves out and before any account state is read. A caller who cannot complete it learns nothing about the account — not whether it has a subscription, when it expires, or which HWID it is bound to.

Setup2FA

Result Setup2FA(const std::string& password = "")

Begins enrolment. Returns a secret and an otpauth_uri: render the URI as a QR code, and show the secret as text for users who cannot scan. Pass the account password when the account has one — changing what it takes to get a session should not ride on a session alone. Accounts that authenticate by licence key have no password, so for those the argument is ignored.

Enrolment is not live until Confirm2FA() succeeds. The secret is held server-side for fifteen minutes and is never retrievable again.

cpp
auto setup = client.Setup2FA(password);
if (setup.ok()) {
    // otpauth_uri -> QR, secret -> manual entry fallback
    ShowEnrolmentScreen(setup.json);
}

Confirm2FA

Result Confirm2FA(const std::string& totp_code)

Commits the enrolment, but only once a code proves the authenticator actually works. That ordering is the point: committing first would let a mistyped or mis-scanned secret lock a paying customer out of their own account.

Backup codes are shown once. The response carries ten single-use backup codes. They are stored hashed and cannot be shown again. Display them, and tell the user to keep them somewhere other than the device running your app.
cpp
auto done = client.Confirm2FA(userTypedCode);
if (done.ok()) {
    ShowBackupCodesOnce(done.json);   // backup_codes[]
}

Disable2FA

Result Disable2FA(const std::string& totp_code)

Turns the second factor off. A currently-valid code (or an unused backup code) is required — a session alone is not enough, because a stolen session is precisely what the second factor exists to stop. Refused outright when the app policy isrequired.


Get2FAStatus

Result Get2FAStatus()

Reports enabled, enrolled_at, backup_codes_remaining, locked and the app's policy, so you can render the right toggle. Never returns the secret or the backup codes.


Two-factor error codes

CodeMeaningWhat to do
TWOFA_REQUIREDCredential was correct; a code is needed.Prompt, then retry Login/License with the code.
TWOFA_INVALIDCode rejected.Let the user retry. The budget is finite — see below.
TWOFA_LOCKEDToo many wrong codes on this account.Show retry_after_ms and stop submitting.
TWOFA_ENROLMENT_REQUIREDApp policy is required; account has no second factor.Route into Setup2FA.
TWOFA_NO_ENROLMENTConfirm2FA called with no setup in progress, or it expired.Start again from Setup2FA.
REAUTH_REQUIREDPassword needed for this operation.Prompt for the password and repeat the call.
Why a wrong code and a reused code look identical. A correct-but-already-used code returns TWOFA_INVALID, not a distinct "already used". Telling a caller their code was right but spent would confirm they hold a valid secret, which is exactly what a replaying attacker wants to learn. Each code is single-use: accepting one records its time-step, and that step and every earlier one are refused afterwards.

Wrong codes are counted per account, not per IP — an attacker with a proxy pool would otherwise have an unbounded budget. Five failures lock the account's second factor for fifteen minutes.


Logout

Result Logout(bool everywhere = false)

Ends the current session. With everywhere = true, ends every session the user holds on this application — the control to offer when someone suspects their account is compromised, or is handing back a shared machine.

cpp
client.Logout();          // this device
client.Logout(true);      // every device

ChangeUsername

Result ChangeUsername(const std::string& new_username, const std::string& password = "")

Renames the account. Usernames are unique per developer and stored lowercased. The password is required when the account has one.

A thirty-day cooldown applies. That is not cosmetic: without one, rename is an impersonation primitive — release a recognisable name for someone else to take, or cycle names to outrun moderation. Previous names are retained so support and abuse work can still resolve them.

CodeMeaning
USERNAME_TAKENAlready in use on your account, or reserved.
USERNAME_INVALIDFails the username rules for this app.
USERNAME_COOLDOWNChanged too recently. next_allowed_at says when.
USERNAME_UNCHANGEDSame as the current name.

Password recovery

Result RequestPasswordReset(const std::string& username_or_email)
Result ResetPassword(const std::string& token, const std::string& new_password)

Recovery is deliberately a two-party flow: Evora issues and verifies the token, you deliver it.

Evora does not email your users. They are your customers, not ours. Sending "your password reset" from an Evora domain about your product is wrong branding, and pooling every tenant's transactional mail onto one sender reputation is a deliverability and abuse liability. In practice almost no end-user accounts carry an email address anyway, so an email-keyed reset would serve nobody.
  • RequestPasswordReset() raises the request to you — a dashboard notification plus an app log entry — and returns success whether or not the account exists. It never returns a token. The uniform response is what stops this being a free account-enumeration endpoint against your whole user base.
  • You issue a token with POST /users/:userId/password-reset and deliver it over whatever channel your customers actually use — commonly a Discord DM.
  • ResetPassword() lets the customer redeem that token inside your app, so recovery never needs a web page. Every live session for the account is ended as the password changes.
cpp
// 1. in-app: user asks for help
client.RequestPasswordReset("username");
// -> always reports success; you receive a dashboard notification

// 2. you issue a token from the dashboard or developer API,
//    and send it to them yourself

// 3. in-app: they paste it
auto r = client.ResetPassword(token, newPassword);

FetchStats

Result FetchStats()

Returns users, licenses, online and versionfor the application — the numbers behind a "1,204 users online" banner.

Off by default. Enable it per app under Settings → Access policies → Publish app statistics. User and licence totals are commercial information and every client binary is in someone else's hands, so this is a disclosure you choose rather than inherit. Until you switch it on the call returns STATS_DISABLED.

When a user loses their authenticator

Backup codes cover the ordinary case. When someone loses both their authenticator and their codes, reset them from Users → ⋯ → Reset two-factor, or DELETE /users/:userId/2fa on the developer API.

This is the only path that removes a second factor without presenting a code, which is why it requires your authenticated developer credentials and is never reachable from the SDK — a self-service "just turn it off" would defeat the feature entirely. The account's live sessions are ended with the reset, on the assumption that the reason for it may have been a compromise.

Security Notice — Read Before Shipping

The Evorion SDK protects what it sees. Everything in your loader's .text that doesn't flow through the SDK is your responsibility. The recipes below are the difference between "cracked in 2 bytes" and "weeks of work per build." Skipping any of them measurably lowers your protection — see Limitations below for the per-recipe failure mode.

What Evorion protects

  • License validation — server-issued, HWID-bound, anti-replay.
  • Session secrets — per-session AES-256 key material derived only when the license is valid.
  • Payload distribution — encrypted blobs you serve to your users, decrypted at runtime only with a valid session.
  • Binary integrity of the SDK itself — internal CFF + sealed cookies + integrity mesh detect tampering of the lib.
  • Anti-replay — session secrets rotate via heartbeat; stolen tokens expire fast.
  • Anti-emulation — best-effort detection of sandboxes and Unicorn-class emulators.

What Evorion CANNOT protect

Evorion is a library. It cannot protect what it never sees.

ThreatWhy Evorion cannot help
Inline plaintext cheat code in your .textIf your aimbot is plaintext machine code at fixed offsets in your binary, no auth check can hide it. Reverser disassembles your loader, finds the cheat, runs it standalone. Refactor required (Recipe 2).
Hardcoded crypto keys in your binaryXOR keys, AES keys, secret salts compiled into your loader will be extracted in minutes. Treat your binary as public.
Plaintext URLs to payloadsEven on private CDNs, plaintext URLs leak via static analysis. Use SessionFetch so the URL is server-issued per-session.
popen("curl ...") / system("...")Visible process command-lines + no TLS validation + child-process injection surface. Use Client::SessionFetch (in-process mbedTLS with cert pinning).
Customer-side branches you "trust"if (license_ok) do_thing() is a 1-byte patch site regardless of how hardened the bool is. Use aes_gcm_decrypt(r, ...) so the success path computes garbage on bypass.
Loader compiled with -O0 / no symbol strippingSymbols and function shapes survive. Strip PDBs; ship optimized release builds. Wrap sensitive functions with EVORION_ENCRYPT_BEGIN/END (post-build) or EVORION_AUTH_PROTECT (runtime auth gate) for real protection.
Crash dumps with debug symbolsWER + minidumps reveal call stacks. Strip PDB, ship with /Brepro and stripped debug info.
Server-side account compromiseIf your evora.lol account is breached, license issuance is the attacker's problem to forge. Treat the account like a code-signing cert.
Nation-state with unlimited time and binary in handOut of scope. The plan slows down adversaries; it does not make reverse engineering impossible.

Recipe 1 — Replace your auth check with the consumer transfer flow

This is the single most important recipe. ~95% of cracks in the wild are this missing step. If you do nothing else from this page, do this.

Wrong — vulnerable to a 1-byte jnz flip

cpp
auto r = client.License(key);
if (!r.ok()) return 1;
// success path — entirely your code:
auto blob = curlDownload("https://my-cdn.example/payload.bin");
for (size_t i = 0; i < blob.size(); ++i) blob[i] ^= kStaticKey[i & 15];
manualMap(blob, "TargetGame.exe");

Right — immune to byte patching (v3.6 transfer flow)

cpp
#include "Evorion.h"
...
auto r = client.License(key);
// NOTE: no early return on !r.ok().  the transfer below IS the gate.
std::vector<uint8_t> blob;
client.SessionFetch("https://payloads.evora.lol/" + r.payload_token(), blob);
std::vector<uint8_t> plain(blob.size() - 28);   // 12B nonce + 16B GCM tag
size_t plain_len = plain.size();
evorion::session::transfer(r, blob.data(), blob.size(),
                            plain.data(), &plain_len);
manualMap(plain, "TargetGame.exe");

If someone patches your jnz, the transfer produces zero bytes and manualMap crashes the target's remote thread. No payload loads. The success path exists only when the session is real; there is no boolean for the attacker to flip.

The transfer call is also tied to your calling site, so patches applied later — to your transfer call site itself, not just an earlier jnz— are detected by the SDK's automatic protection layers and result in an unrecoverable session.

For customers who ship assets bundled with their build (rather than fetched per-session), use session::bind_key(r, label, len, out) to derive a stable 32-byte key tied to the session; encrypt at build time with the same label, decrypt at runtime with the returned key. Between transfer and bind_keythe entire "auth-gated content delivery" problem is covered — you should not need any other primitive. Older integrations calling aes_gcm_decrypt keep working for payloads already shipped, but the compiler will warn at each call site so new code migrates to transfer.
Recipe 1 is an architectural pattern, not a drop-in replacement for if (r.ok()). session::transferdoes not return "may I proceed" — it decrypts. On a bypass, the output is 32 zeros, and the gate is your success path panicking naturally on garbage input. That only holds when your success path is actually coupled to the decrypted material: a manual-map target, a JIT blob, a decryption key for the routine main() calls next. If your app just does "log in and run my normal features" with nothing server-encrypted in between, there is nothing for transfer to gate.

For that case, the fix is not to try to force transferin — it is to identify what in your success path is worth gating on the session and route it through bind_key: a config that resolves your endpoints, a feature-flag decoder, the licence-check routine itself, a small piece of hot logic. Encrypt it at build time under a labelled bind_key derivation; at runtime, main() cannot function until a real session yields the same key. The point of both primitives is the same: make the success path structurally depend on cryptographic material only a genuine session produces, so there is no boolean between "got a session" and "did the work" for an attacker to patch.


Automatic protection

The SDK arms a set of runtime protections the moment Evorion.lib is linked into your build. There is nothing to call, configure, or enable. These layers are the safety net for integrations that ship without the post-build packer — they raise the cost of a scripted crack from seconds to hours plus detection risk. They do not substitute for shipping through evora-protect for high-value binaries.

Tamper detection at your call sites

When your code calls into any Evorion gate, the SDK ties the check to the surrounding bytes of your calling code. If those bytes are modified later — a static patch of your binary, an in-memory hook, or a runtime rewrite — the session becomes unrecoverable and the process is retired unpredictably. You do not need to know when or how; the check is continuous for the lifetime of the process.

The transfer flow in Recipe 1 pairs with this layer. Use both: the transfer flow removes the branch an attacker would try to patch in the first place, and this layer detects patches applied to whatever code paths remain.

Anti-dump

Standard process-dumping tools that scan memory for a binary's signature, reconstruct its import table, or rebuild a runnable file from a paused process do not succeed against an Evorion-linked build. Attempts to attach and dump produce broken output that will not run. Repeat attempts do not converge on a working dump — the protection is re-applied continuously in the background.

Anti-debug and anti-instrumentation

Debuggers, memory scanners, and known reverse-engineering tools observed alongside the protected process cause it to become unrecoverable. This begins before your main()runs, so an attacker cannot "attach before the checks start." Detection is silent — there is no message box, no clean exit code, no stack trace back to a check site.

The detection surface is deliberately curated to avoid legitimate development tools a sysadmin might have open. If you encounter a false positive that trips on a mainstream tool, contact [email protected].

What these layers do not replace

The automatic layers are runtime defences. They do not:

  • Protect inline plaintext logic in your loader's .text (see Recipe 2).
  • Substitute for post-build packaging via evora-protect, which additionally rewrites call sites, encrypts sections at rest, and lifts sscx-marked functions to server-side execution.
  • Defend against kernel-mode attackers with unlimited time. See Limitations below.

Recipe 2 — Refactor inline sensitive code out of your loader

If your sensitive code is inline C++ in your loader's .text, the SDK physically cannot protect it. Someone who flips your jnz reaches your inline instructions directly, with no encryption to defeat.

Lift it:

  • Move the sensitive logic into a separate DLL or position-independent shellcode.
  • Encrypt the blob at build time with a session-derived key (any AES-GCM tool; the key material is your business — the SDK never sees your build-time blobs).
  • Serve it from your CDN behind a signed URL your server issues per-session.
  • At runtime: Client::SessionFetch → evorion::session::transfer(r, ...) → VirtualAlloc PAGE_EXECUTE_READWRITE → memcpy → call.

Someone who patches your jnz gets garbage instructions in RWX memory; the process crashes on the first call.


Recipe 3 — Configure heartbeat for resident loaders

cpp
evorion::Client client(
    owner_id, app_id, "1.0.0",
    TransportMode::Http,
    /*auto_init=*/true,
    /*heartbeat_interval=*/30,   // seconds — keep ≤ 30 for live sessions
    /*antidebug_interval=*/500,
    /*auto_exit=*/true);

Heartbeat rotates session_secret. Mid-session license revocation invalidates the secret on the next tick — anti-replay is automatic. Server-side OnPush callbacks for kill/ban terminate the session out of band on the same channel.


Do — explicit list

  • Use evorion::session::transfer(r, ...) to deliver any server-issued content the customer's success path actually uses. It is the right answer for "receive authenticated bytes and use them" — but only when your success path is coupled to those bytes. If it isn't, see Recipe 1's architectural note.
  • Use evorion::session::bind_key(r, label, len, out) when you ship encrypted assets bundled with your build — encrypt at build time with the same label, decrypt at runtime with the returned key. This is the ONE right answer for "session-bound derived key."
  • Use Client::SessionFetch(...) for all downloads (in-process TLS, cert pinned).
  • Wrap sensitive code with EVORION_ENCRYPT_BEGIN/END for post-build encryption, EVORION_AUTH_PROTECT for auth-gated blocks, and EVSK() for compile-time string encryption.
  • Auth-gate sensitive constants with EVORION_LOCKED_INT. .load() consults live session state — a bypassed session returns a zero-initialized value.
  • Set heartbeat_interval ≤ 30 seconds for live / resident loaders.
  • Strip symbols, PDB paths, and debug info from release builds.
  • Build with deterministic linker flags (/Brepro) so leaked timestamps don't fingerprint your machine.
  • Ship optimized release builds (-O2 at minimum) — unoptimized code preserves function shapes and symbol structure.
  • Implement Recipe 1 even if you don't think you need it — it costs 5 lines.
  • Keep evora.lol account credentials in a hardware-backed secret store.
  • Rotate license keys quarterly.
  • Monitor your owner dashboard for unexpected HWID changes and failure-rate spikes.

Don't — explicit list

  • Don't hardcode XOR keys, AES keys, or salts in your loader.
  • Don't write static URLs to payload CDNs (use SessionFetch + server-issued URL).
  • Don't use popen / system / ShellExecute / CreateProcess for downloads.
  • Don't use curl -k (insecure flag disables TLS validation — defeats the entire chain).
  • Don't gate your success path on if (r.ok()), if (client.Authenticated()), or any other if (some-boolean-derived-from-auth). The pattern is the vulnerability; the compiler will warn you at every such call site. The right shape is to make the success path structurally depend on cryptographic material only a real session can produce — via session::transfer (server-issued content) or session::bind_key (build-time-bundled content), whichever matches your app. See Recipe 1 for the case where neither naturally fits.
  • Don't compare an HMAC or key you derived from the session against an expected value in an if. If you find yourself writing if (memcmp(computed, expected) == 0) work();, you have re-created the same patchable branch. Use the derived value as key material for whatever comes next (bind_key + your own AEAD).
  • Don't store sensitive code inline in your loader's .text as plaintext.
  • Don't log r.session_secret() bytes — not to stdout, stderr, files, debug output, or anywhere persistent.
  • Don't cache decrypted payloads on disk — they're in RWX memory only.
  • Don't ship debug builds (EVORION_VERBOSE defined) to customers — log strings leak everything.
  • Don't compile with PDB paths reachable from the binary's .rdata (use /PDBALTPATH:%_PDB%).
  • Don't ship OutputDebugString calls in release builds — debuggers attach silently to read them.
  • Don't bundle evr_diag.log / evr_crash.dmp in your release artifacts.
  • Don't roll your own anti-debug on top of textbook checks — the SDK already covers anti-debug automatically and layered ad-hoc checks are trivially bypassed while adding maintenance cost.
  • Don't reuse license keys across customers (the server rotates on misuse).

Limitations — what happens if you skip each recipe

If you skip……expected outcome
Recipe 1 (session::transfer)1-byte jnz flip and your success path runs anyway. Same crack that triggered this whole hardening exercise. The automatic protection above raises the cost but is not a full substitute for using the transfer flow.
Recipe 2 (refactor inline)The SDK runs perfectly, returns a valid session_secret, the flipped jnz skips your decrypt call and runs your inline logic anyway. Auth becomes decorative.
Recipe 3 (heartbeat)Stolen session_secret works until process death (could be hours). No revocation.
Strip symbols / PDBYour function names + paths land in the release binary. Reverser already has half of IDA's job done for them.
EVORION_VERBOSE left on in releaseLog file evr_diag.log written to disk; debug-string artifacts in .rdata; failure reasons leaked to any reader.
TLS pinning / SessionFetchMitM swaps your payload; you become a vector.
Even with all recipes followed, Evorion does NOT guarantee absolute resistance: a nation-state with unlimited time and your binary in hand will eventually defeat any commercial protection. The plan slows them down; it does not stop them. If evora.lol's server is compromised, attackers can issue valid sessions — your defense at that point is account hygiene, not the SDK. Side-channels (timing, power, EM) are out of scope for a userspace library.

If you suspect a crack

  • Don't panic — file a ticket at [email protected] with the leaked binary if you have it.
  • Check your build — verify Recipes 1 + 2 are in place; ~95% of cracks are missing Recipe 1.
  • Check telemetry — your owner dashboard shows per-license HWID counts and failure-rate spikes; a sudden spike usually precedes a public crack by hours.
  • Rotate — push a build with new payload encryption keys and new license issuance keys. Old cracked builds stop working on the next session refresh.
  • Report indicators — share tool signatures with evora.lol; we update the global blocklist.

Reporting a vulnerability in the SDK itself

[email protected]. PGP key in your owner dashboard. Bounty program details on the Trust page.


Glossary

The two consumer primitives

Every legitimate customer use case maps to one of these two. If you are reaching for anything else, you are either rebuilding the patchable-boolean pattern by hand or duplicating what one of these already does.

TermMeaning
session::transfer(r, in, in_len, out, out_len)Receive server-issued authenticated bytes. AEAD binds authenticity + confidentiality in one operation; output IS the payload the customer's success path needs. On a bypass, output is zero-filled. There is no verify step to branch on — the crypto output is the gate.
session::bind_key(r, label, len, out_key)Derive a 32-byte per-purpose key tied to the session, for encrypting assets you ship with your build. Encrypt at build time with the same label; decrypt at runtime with the returned key. The customer's downstream AES-GCM decrypt succeeds only when the derived key matches — no branch, the key IS the gate.

Supporting API

TermMeaning
Result::session_secret()32-byte key material populated only when the session is genuine. Consumed internally by the two primitives above. Do NOT touch it directly — indexing / comparing / branching on session_secret bytes rebuilds the same patchable-boolean footgun the transfer flow eliminates.
SessionFetchIn-process TLS-pinned download. Replaces popen("curl ...").
Session secretPer-session 32-byte derived key material rotated by heartbeat. Feeds both consumer primitives.
HWIDHardware ID hash derived from CPUID + SMBIOS + TPM endorsement key. License is bound to first-seen HWID by default.
HeartbeatPeriodic re-auth that rotates the session secret. Default 30 s. Reduce for higher-security flows.
Automatic protectionThe set of runtime defences (tamper detection, anti-dump, anti-debug) that fire once Evorion.lib is linked, without customer configuration. See the Automatic protection section for what it covers.

Legacy — do not use in new integrations

These names are still callable so existing integrations compile and existing payloads decrypt. All emit compiler deprecation warnings at the call site; migrate to session::transfer or session::bind_key.

TermWhy not to use
Result::ok()Boolean auth status. One-byte JZ→JMP patch and the success path runs anyway. Structurally couple your success path to session-derived cryptographic material (session::transfer for server-issued content, session::bind_key for build-time-bundled content) rather than to this bool. See Recipe 1's architectural note.
Client::Authenticated()Same shape as Result::ok(). Same crack. Same fix.
session::aes_gcm_decrypt(r, ...)Predecessor of session::transfer with a different AAD. Kept so existing payloads decrypt; new code should use session::transfer.
session::hmac_verify(r, ...)Returned a bool-shaped ErrorCode. Customers wrote `if (hmac_verify(...) == None) { work; }` — the same patchable branch we're eliminating.
session::compute_hmac(r, data, len, out)Standalone HMAC primitive. Only load-bearing when the output is used as key material downstream — and that use case is already covered by bind_key + your own AEAD. If you find yourself using compute_hmac, you almost certainly wanted bind_key.
xor_in_placeRemoved. The old body XORed against session_secret; on a bypass leaving the secret at zeros, output equalled input — a total defeat of the load-bearing model. Symbol is gone; call sites now fail to compile.

Anti-Debug & Anti-TamperPro / Ultra

The SDK runs anti-debug scans in a background thread at the interval you specify. Detections are reported to the server. When Auto-Ban is enabled in the dashboard, the device gets blacklisted immediately.

cpp
evorion::Client client(
    owner, app, ver,
    evorion::TransportMode::Http,
    true,   // auto_init
    30,     // heartbeat
    500,    // anti-debug scan every 500ms
    true    // auto_exit - kills process on detection
);
// anti-debug is now running. nothing else to do.

What it covers

  • User-mode and kernel debuggers attached to the process.
  • Runtime code and API hooking.
  • In-memory tampering with the loader's code.
  • Emulator and virtualisation environments used to bypass runtime checks.
  • Manual mappers and other non-standard module loading.
The specific mechanisms behind each detection change between releases and are intentionally not documented — the surface visible to an attacker reading these docs is the smallest possible slice of what the SDK actually enforces.

Binary Integrity & Remote AttestationPro / Ultra

Server-driven memory attestation verifies that your application's code hasn't been modified at runtime. The server holds a "golden image" of your binary's .text section and periodically challenges connected clients to prove their in-memory code matches.

How it works

  1. Upload. Upload your compiled binary (or pre-extracted .text section) to the dashboard. The server extracts and stores the .text section encrypted with AES-256-GCM.
  2. Enable. Toggle integrity checking on for your application in Binary Integrity settings.
  3. Challenge. During heartbeats, the server picks random regions of .text and sends an attestation challenge with a fresh nonce.
  4. Proof. The SDK reads its own memory at those offsets, computes HMAC-SHA256(regions || nonce, BUILD_SECRET), and returns the proof.
  5. Verify. Server verifies the proof against its golden image. Mismatch → IntegrityViolation.

Dashboard setup

Navigate to your app's Binary Integrity page. Two upload modes:

ModeDescription
Binary UploadDrag & drop your compiled .exe, .dll, or .sys. The server automatically extracts the .text section via its PE parser.
Hash UploadPaste a pre-extracted .text section as base64. Use this if you extract the .text section yourself or from a CI pipeline.
The version you specify when uploading must match the version string your SDK client reports in its constructor.

SDK integration

Remote attestation is fully automatic. Once integrity checking is enabled and a golden image is registered for your version, the SDK handles challenges during heartbeats with zero extra code on your part.

cpp
// no special code needed, attestation runs inside Heartbeat()
EVORION_CLIENT(client,
    "OWNER_ID",
    "APP_ID",
    "1.0.0",
    evorion::TransportMode::Http,
    true, 30, 500, true
);

if (!client.Initialized()) return 1;
auto login = client.Login("user", "pass");
if (!login.ok()) return 1;

while (running) {
    auto hb = client.Heartbeat();
    if (!hb.ok()) {
        if (hb.error_code == evorion::ErrorCode::IntegrityViolation)
            std::cerr << "Integrity violation detected\n";
        break;
    }
    Sleep(30000);
}

Attestation protocol

PropertyDetail
RegionsRandom .text offsets per challenge, can't precompute
Nonce32-byte fresh nonce per challenge, prevents replay
AlgorithmMultiple HMAC variants (0–3), defeats generic hash emulators
SecretHMAC keyed with build secret, can't forge without extracting it

Golden image self-registration

When your app connects for the first time and no golden image exists for its version, the SDK automatically extracts its own in-memory .text section and uploads it to the server. No manual upload needed for every build.

If you use packers (Themida, VMProtect, etc.), upload the fully packed binary. Self-modifying code that alters .text at runtime will cause false positives.

Golden image CLI tool

For CI/CD pipelines, use the extract-golden-image.mjs tool (shipped in sdk/tools/) to extract and upload golden images as a post-build step:

cpp
# Extract from compiled binary and upload
node extract-golden-image.mjs MyApp.exe <app_id> <version> --api-key <key>

# For packed binaries: dump at runtime, then upload the dump
MyApp.exe --dump-golden golden.bin
node extract-golden-image.mjs golden.bin <app_id> <version> --raw --api-key <key>

# Extract only (don't upload)
node extract-golden-image.mjs MyApp.exe <app_id> <version> --dump-only

Hardware ID

std::string evorion::hwid::Generate()

Generates a stable hardware fingerprint for the current machine. Used internally for HWID lock, but you can also use it for your own device tracking.

cpp
std::string hwid = evorion::hwid::Generate();
std::cout << "Device: " << hwid << "\n";

Secure Credentials

Plaintext strings sit in the binary at rest. SecureCredentialencrypts them at compile time so there's nothing to find in a hex editor or string dump.

EVSK macro

The fastest way. Wrap your owner id or any other sensitive literal into a SecureCredential with compile-time encryption:

recommended approach
evorion::Client client(
    EVSK("your-owner-uuid"),
    "your-app-uuid",
    "1.0"
);

EVORION_CLIENT macro

All-in-one shorthand that wraps the owner id automatically and forwards the remaining constructor arguments:

one-liner
EVORION_CLIENT(client, "owner-uuid", "app-uuid", "1.0", evorion::TransportMode::WebSocket);

SecureCredential class

MethodDescription
FromHex(hex, key_seed)Construct from hex-encoded encrypted blob
FromPlain(text)Wrap plaintext (debug/testing only)
decrypt()Decrypt and return plaintext
has_value()Whether the credential contains data

Transport Modes

The SDK supports two transport modes:

ModeUse Case
TransportMode::HttpStandard HTTPS request/response. Simpler, works behind restrictive firewalls. Default.
TransportMode::WebSocketPersistent connection. Enables server push (OnPush()), remote kill/ban, real-time variable updates.
All API methods (Login, Check, GetVar, DownloadFile, etc.) work identically on both transports. The only difference is that WebSocket enables OnPush() callbacks.

Version Management

The server checks the SDK version and your app version on every Init(). Configure version policies per-application in the dashboard:

PolicyBehavior
okVersion is accepted. Normal operation.
warnVersion is outdated but allowed. Init() succeeds. The server may include an update_url in the response.
blockVersion is rejected. Init() fails with ErrorCode::VersionBlocked. The user must update.
Set the versionparameter in the constructor to your app's actual version string. The server matches against the version rules you configure per-app in the dashboard.

Code ProtectionPro / Ultra

When someone reverse engineers your program, they open it in a disassembler (like IDA Pro or Ghidra) and read the compiled instructions to understand what your code does. Code protection makes that harder. Every macro below does actual work — either at build time (server-side encryption via evora-protect) or at auth-tied runtime (auth gates + dynamic API resolution). Runtime-only CFF wrappers were removed in R70 — they added noise without a post-build lift.

MacroWhat it doesWhen to use
EVORION_ENCRYPT_BEGIN / ENDByte-marker in your .text. Post-build, the evora-protect CLI encrypts everything between the markers with a server-managed AES-256-GCM key. Runtime decrypts on demand.Any block you want the CLI to encrypt. Keys revocable per build.
EVORION_AUTH_PROTECT(client, name) + PROTECT_DONE(name)Wraps a block in a runtime-verified auth gate: _evr_vc(client, site_id) at entry, fail-closed if the session is invalid.License checks and any code that must not run when the session isn't real.
EVORION_LOCKED_INT/UINT/INT64(client, name, value)Auth-gated constant. .load() consults live session; invalid session returns a zero-initialized value.Numeric config that should only be correct after real auth.
EVORION_DYNAPI(dll, api)Resolves WinAPI addresses through a PEB walk + export table scan — no IAT entry. Static analysis can't see which APIs you call.Any WinAPI you'd rather not advertise in your import table.
sscx marker (on function)Opts the function into MAX-tier server-side shield-VM lift performed by evora-protect. The body doesn't ship in your binary at all — server executes it and returns the result.The highest-value single functions in your build. Cost: one server round-trip per call.
All protection macros compile to nothing when EVORION_NO_PROTECT is defined, so debug builds are clean and fast.

Code Encryption (EVORION_ENCRYPT)Pro / Ultra

This is different from the above. Instead of transforming the structureof your code, it encrypts the actual bytes in the binary with AES-256-GCM. The decryption keys live on the Evora server, not in the binary. At runtime the SDK fetches keys from the server and decrypts each section in memory automatically when execution reaches it. This is server-controlled code protection. If someone dumps your binary, the encrypted sections are unreadable without server access. You can revoke a build's keys at any time, permanently disabling that version.

How it works

  1. Mark code sections with EVORION_ENCRYPT_BEGIN / EVORION_ENCRYPT_END in your source. The markers compile to harmless jumps, so the code runs normally before protection.
  2. After compiling, run the EvorionShield CLI tool on your binary. It finds the markers, encrypts each section with AES-256-GCM, uploads the keys to the Evora server, and patches the markers into decrypt stubs.
  3. At runtime, call EVORION_SHIELD_INIT after authentication. This registers the auto-decrypt callback. From that point on, when execution reaches an encrypted section the SDK automatically fetches the key and decrypts in-place.
  4. The section is decrypted in memory only. The binary on disk stays encrypted.
cpp
// after authenticating, init the shield runtime
EVORION_SHIELD_INIT(client);

EVORION_ENCRYPT_BEGIN;
    do_critical_work();
EVORION_ENCRYPT_END;
The server rate-limits decrypt requests per device (100 calls/min, 10 unique sections/min) and logs every decrypt with device ID, session, IP, and HWID. You can revoke a build's keys from the developer dashboard or API to permanently block that version.
layered protection
EVORION_ENCRYPT_BEGIN;
    EVORION_AUTH_PROTECT(client, my_algo)
        run_sensitive_algorithm();
    EVORION_PROTECT_DONE(my_algo)
EVORION_ENCRYPT_END;

Call EVORION_SHIELD_SHUTDOWN(client)when you're done to clean up the shield connection.


Encrypted Strings

Strings are one of the easiest things to find in a binary. SecureStr eliminates this attack surface entirely with two layers of protection:

  • Compile-time encryption removes the plaintext from the binary. The string never appears in .rdata or any other section.
  • Runtime encryption keeps the string sealed in DPAPI-encrypted, VirtualLock'd memory. Plaintext only exists during a scoped access window, then is wiped immediately.
cpp
auto url = SecureStr("api.evora.lol/v3/auth");

// plaintext exists only inside this scope
{
    auto v = url.access();
    http_get(v.ptr());
}
// plaintext wiped from memory here

// one-liner (wiped after the full expression)
http_get(url.use());

For wide strings, use WSecureStr(L"..."). Same protection, same API.

cpp
auto path = WSecureStr(L"C:\\secret\\config.dat");
{
    auto v = path.access();
    load_config(v.ptr());
}
Do not construct SecureString with a raw string literal directly (e.g. SecureString("...")). That only encrypts in memory at runtime, but the literal still sits in the binary's .rdata section. Always use SecureStr() to get both compile-time and runtime protection.

SecureStringPool

If you have multiple strings used in hot paths and want to avoid DPAPI + VirtualAlloc overhead on first use, pre-allocate them at startup with a pool:

cpp
static evorion::sstr::SecureStringPool<3> urls;

void Init() {
    urls.set(0, skCrypt("api.evora.lol/v3/auth"));
    urls.set(1, skCrypt("api.evora.lol/v3/heartbeat"));
    urls.set(2, skCrypt("api.evora.lol/v3/validate"));
}

void SendAuth() {
    http_get(urls.get(0).use());
}

Auth-Protected Blocks (EVORION_AUTH_PROTECT)

EVORION_AUTH_PROTECT wraps a block in a runtime-verified auth gate. At entry, it calls _evr_vc(client, site_id) — a live-session validity check computed against the current heartbeat state and mesh entanglement. If the session is invalid (server kill, blacklist, heartbeat expiry, mesh trip), the block calls FailClosed immediately. Patching the auth flow at any earlier point does not defeat the gate — the vc is computed fresh at every entry.

Usage

cpp
void run_premium_feature(evorion::Client& client) {
    EVORION_AUTH_PROTECT(client, premium)
        do_premium_stuff();
        apply_results();
        save_state();
    EVORION_PROTECT_DONE(premium)
}

Pair with EVORION_PROTECT_DONE(name) to close. The block runs only when the session is genuine; on invalid auth the process fail-closes rather than silently continuing.

Companion primitives

MacroBest for
EVORION_REQUIRE_AUTH(client)Quick inline check. Returns Client::Authenticated() so you can early-return on failure.
EVORION_AUTH_PROTECT(client, name)Wrapped auth gate around a block of code. Fail-closed on invalid session.
EVORION_LOCKED_INT / _UINT / _INT64(client, name, value)Auth-gated constant. .load() consults live session state — invalid auth returns a zero-init value.

Dynamic API hide your imports

Strip Win32 functions from your binary's import address table without rewriting your code. The SDK already does this for ~185 common APIs automatically; for anything else, one macro covers it.

What it does

  • Function and DLL names hashed at compile time. No plaintext strings in your binary.
  • Resolution walks the loaded-module list + target DLL's export table directly (no GetModuleHandle/GetProcAddress in your IAT).
  • Resolved pointer cached per call-site in an XOR-obfuscated slot keyed on a per-process secret from the system PRNG.
  • Cheap anti-hook prologue check rejects pointers redirected via JMP rel32, MOV RAX imm64 / JMP RAX, INT3, etc.
  • Failed resolutions retry after a short timeout instead of becoming permanent nulls.

Primary usage — automatic via header

Just include <Evorion.h> and write normal Win32 code. The header macro-shadows ~185 functions across kernel32, user32, advapi32, ntdll, bcrypt, winhttp, crypt32, dbghelp, gdi32, gdi+, shell32, shlwapi, ws2_32, iphlpapi, ole32, oleaut32, psapi, tbs, and more. None of those calls leave an IAT entry.

cpp
#include <Evorion.h>

void DoSomething() {
    Sleep(1000);                            // de-imported automatically
    HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, 4);
    IsDebuggerPresent();
    CloseHandle(h);
}

Escape hatch — APIs not pre-shadowed

For any export the SDK does not auto-shadow, use the EVORION_DYNAPImacro. The function's declaration must already be visible (e.g. you included the right Windows header) so the compiler can deduce the type and calling convention. You never write the typedef yourself.

cpp
#include <Evorion.h>
#include <wininet.h>     // declares InternetCheckConnectionW

void NetCheck() {
    auto p = EVORION_DYNAPI(wininet.dll, InternetCheckConnectionW);
    if (p) p(L"https://example.com/", FLAG_ICC_FORCE_CONNECTION, 0);
}

Lazy variant — late-loaded DLLs

For a DLL that may not be loaded yet at the call site, use EVORION_DYNAPI_LAZY. The DLL is loaded from %WINDIR%\System32 only — never from the application directory — so DLL planting cannot redirect the call.

cpp
#include <Evorion.h>
#include <wincrypt.h>

void DpapiRoundTrip(BYTE* buf, DWORD len) {
    auto p = EVORION_DYNAPI_LAZY(crypt32.dll, CryptProtectMemory);
    if (p) p(buf, len, CRYPTPROTECTMEMORY_SAME_PROCESS);
}
Use literal symbols only. The macro is fed a bare function name and an unquoted DLL — both are stringified at preprocess time. Passing a variable, a runtime-built name, or anything attacker-influenced defeats the IAT-hiding goal AND opens a DLL planting vector.

What it does NOT protect

  • Not a sandbox. Once you have the function pointer, the call is a normal Win32 call.
  • A dynamic analyst running your binary in a debugger sees every call. The function pointer is hidden, the execution is not.
  • Hiding the import buys you obscurity, not invisibility. Behavioral patterns still telegraph intent.

Performance

First call per slot: ~200–500 ns (PEB walk + export-table scan). Subsequent calls: one XOR load — same cost as a normal function-pointer call. Per-process secret seeding is a one-time cost at SDK init.

Custom API domainPro / Ultra

By default your app talks to api.evora.lol. A custom domain lets you serve the exact same API from a hostname you own — auth.yourgame.com— so your users' traffic never names Evora. Two things this buys you: if an ISP or network firewall blocks evora.lol, your app keeps working; and a casual look at your binary or its network traffic no longer reveals which auth provider you use.

Nothing about the API changes — same endpoints, same keys, same request bodies, same responses. Only the hostname on the wire is different. You can switch back at any time.

What you need

RequirementDetail
A domain you ownAny registrar — Cloudflare, Namecheap, GoDaddy, Porkbun. You do not need to move the domain to Cloudflare.
A spare subdomainUse one you are not already serving a website from, like auth. or api. Do not use your apex/root domain.
Pro plan or aboveCustom domains provision a dedicated certificate per app, so they start at Pro.

Connecting a domain

Open your application, go to Settings → Custom Domain, and enter the hostname you want to use. Leave off https:// — just the bare name.

  1. Enter auth.yourgame.com and click Connect domain. The panel shows a status of Waiting on DNS.
  2. Add the DNS records the panel shows you at your registrar (details below). There is one CNAME that does the routing, and usually one TXT that proves you own the domain.
  3. Come back to the panel. It re-checks every few seconds — you do not need to refresh. Status moves to Verifying while the certificate is issued, then Live.
  4. Point your app at the new hostname (see below) and ship an update.

The DNS records

The exact values are generated per domain and shown in the panel — copy them from there. They take one of these shapes:

TypeNamePoints toWhy
CNAMEauth.yourgame.comssl.evora.lolRoutes your hostname to the API.
TXT_cf-custom-hostname.auth.yourgame.com(token shown in panel)Proves you own the domain so a certificate can be issued for it.
If your DNS is hosted at Cloudflare, set the CNAME record to DNS only (grey cloud), not proxied. A proxied record here creates a certificate loop and the domain will stay stuck on Verifying.

How long it takes

DNS changes are usually visible within a few minutes but can take up to an hour depending on your registrar. Once the records are found, the certificate is typically issued within a minute or two. If a domain sits on Waiting on DNS for more than an hour, re-check that the record Name and Value match the panel exactly — a trailing dot or an extra .yourgame.com appended by your registrar is the usual cause.


Using it — REST API

Once the domain is Live, swap the base URL everywhere you call the API. Nothing else changes.

diff
- https://api.evora.lol/api/v1/auth/login
+ https://auth.yourgame.com/api/v1/auth/login

Every endpoint, header, API key, request body and response is identical. If you keep a base URL in a config value, this is a one-line change.

javascript
// before
const API = "https://api.evora.lol/api/v1";

// after — your own domain
const API = "https://auth.yourgame.com/api/v1";

const res = await fetch(`${API}/auth/login`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ app_id, key: licenseKey }),
});

Using it — C++ SDK

The SDK talks to api.evora.lol by default. Call Client::SetApiHost() once, before you construct your client, with the bare hostname. That is the whole change.

cpp
#include "Evorion.h"

int main() {
    // Route through your connected custom domain. Bare hostname — no
    // "https://", no path. Call this BEFORE constructing the client.
    evorion::Client::SetApiHost("auth.yourgame.com");

    evorion::Client client("OWNER_ID", "APP_ID", "1.0");
    if (!client.Init()) return 1;
    // ... everything else is unchanged
}

Certificate pinning still works — automatically

The Evorion 4 Umbra SDK pins the TLS certificate chain for defence in depth. You do not need to change, rebuild, or ship anything for pinning to keep working on a custom domain: the SDK pins the long-lived chain anchors (the certificate authorities), not the short-lived leaf certificate. The certificate issued for your hostname chains to the same authorities, so it validates against the existing pins with nothing to rotate.

This is why custom domains need no per-customer SDK build. Response authenticity does not rest on TLS anyway — every server reply is signed with a key compiled into your SDK, so a forged certificate buys an attacker nothing.

No silent fallback

If your custom domain stops resolving, the SDK does not quietly fall back to api.evora.lol — doing so would leak the very hostname you are paying to hide. Init() fails instead. Ship a build you can update, and treat a connect failure as a signal to check your domain rather than something the SDK papers over.

To confirm a custom host actually took effect in a diagnostic build, read it back:

cpp
evorion::Client::SetApiHost("auth.yourgame.com");
// prints "auth.yourgame.com"
std::printf("api host: %s\n", evorion::Client::GetApiHost().c_str());

Disconnecting

Remove a domain from Settings → Custom Domain. The hostname stops answering API requests immediately, so ship an update that points back at api.evora.lol first— any build already in your users' hands that still points at the removed domain will break. Re-connecting the same domain later re-issues its certificate from scratch.

Developer API

HTTP API at https://api.evora.lol/api/developer-api for the same operations as the dashboard: users, licenses, webhooks, stats, and more. Every route requires a valid API key with the right scopes.

Base URL

cpp
https://api.evora.lol/api/developer-api

Quick start

cpp
curl -H "Authorization: Bearer ag_sk_YOUR_KEY" \
  https://api.evora.lol/api/developer-api/users
cpp
# generate 10 licenses, 30 days each, level 1
curl -X POST https://api.evora.lol/api/developer-api/apps/YOUR_APP_ID/licenses \
  -H "Authorization: Bearer ag_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"amount": 10, "duration": 30, "expiry": 86400, "level": 1}'
cpp
# ban a user
curl -X POST https://api.evora.lol/api/developer-api/users/USER_ID/ban \
  -H "Authorization: Bearer ag_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"reason": "TOS violation"}'
Duration is duration × expiry seconds. expiry is the unit multiplier — 86400 for days, 3600 for hours, 60 for minutes. Set duration: 0 for a lifetime key. Unknown fields are ignored, so a typo silently yields a default 1-day key — copy the shape above exactly.

Response shape

Responses are plain JSON objects; most list endpoints return their collection at a named key (for example { apps, total, page, limit }). Errors use standard HTTP status codes with { error: '...' }, and several endpoints add a stable code field you can branch on.

Rate limits

A fixed 300 requests/minute per-key ceiling, plus your plan's requests-per-minute quota — the latter is the limit you'll normally meet. Over either, you get 429 with retry_after (seconds). Expired or over-quota subscriptions return 402.


Recipe: a Discord bot

The bot holds the API key on your server and never exposes it. A typical purchase-to-access flow is three calls: create the customer, generate a key, redeem it for them.

Node — /redeem command
const API = 'https://api.evora.lol/api/developer-api';
const H = {
  'Authorization': `Bearer ${process.env.EVORA_KEY}`,
  'Content-Type': 'application/json',
};

// 1. Create the customer account (password is required, 6+ chars)
const { user } = await fetch(`${API}/users`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ username: discordName, password: generatedPassword }),
}).then(r => r.json());

// 2. Redeem a key on their behalf — server-to-server, no password needed
const res = await fetch(`${API}/users/${user.id}/redeem`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ licenseKey: keyFromCustomer }),
});
const body = await res.json();

if (!res.ok) {
  // Stable codes: INVALID_KEY, KEY_BANNED, KEY_PAUSED, ALREADY_REDEEMED,
  // NOT_FOR_THIS_USER, HWID_MISMATCH, NO_BENEFIT, QUOTA_EXCEEDED
  return reply(`Could not redeem: ${body.error}`);
}
reply(`Activated ${body.data.subscriptionName} until ${body.data.expiresAt ?? 'forever'}`);

Redemption here behaves exactly as it does in the loader and the account panel — same validation, same no-benefit protection, same events. Point a webhook at your bot to hear license.used regardless of where the redemption happened; the payload's source field tells you which surface it came from.

Linking Discord accounts

There is no built-in Discord ID field. Store it as a per-user variable and look users up by username:

cpp
# tag the Evora user with their Discord id
curl -X POST "$API/apps/$APP_ID/users/$USER_ID/variables" \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"var_key": "discord_id", "var_value": "123456789012345678"}'

# later: resolve a customer by name
curl "$API/apps/$APP_ID/users/lookup?username=someuser" -H "Authorization: Bearer $KEY"

Recipe: a customer panel

Your panel's backend holds the API key and proxies every call. The browser must never see an ag_sk_ key — CORS blocks direct browser calls precisely so this mistake is hard to make.

Panel login → your own session
// Verify the customer's credentials against Evora
const r = await fetch(`${API}/apps/${APP_ID}/users/authenticate`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ username, password }),
});
const data = await r.json();
if (!data.authenticated) return renderLoginError();

// Evora does not issue a browser session — you mint your own cookie here.
req.session.evoraUserId = data.user.id;

// data.subscription: { level, name, expiresAt, hwid, paused, active }
// The "active" flag is false when expired OR paused — the same verdict the SDK reaches.

To let a customer launch the protected app straight from your panel, mint a one-time, HWID-bound exchange token and hand it to your loader, which trades it for a real SDK session. Your panel never handles the customer's password.

Panel SSO into the SDK
// 1. Panel backend mints the token (TTL 30-300s, one-time use)
const { exchange_token } = await fetch(
  `${API}/users/${userId}/issue-session-token`,
  { method: 'POST', headers: H,
    body: JSON.stringify({ appId: APP_ID, hwid: clientHwid, ttlSeconds: 120 }) },
).then(r => r.json());

// 2. Your loader posts it to the SDK proxy endpoint to obtain a session
//    POST /api/v2/proxy/login-by-token  { exchange_token, hwid }
The token is bound to the exact HWID you pass and can be redeemed once. It is refused for banned users and for users without a subscription to that app.

Recipe: a panel for a license-only app

If your app uses auth_mode: license, your customers have no username or password — their key is their credential. Authenticate them with the key instead; everything after that point is identical to the user/password panel above, because both endpoints return the same user and subscription envelope.

Panel login by license key
const r = await fetch(`${API}/apps/${APP_ID}/licenses/authenticate`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ licenseKey: keyFromCustomer }),
});
const data = await r.json();

if (data.authenticated) {
  req.session.evoraUserId = data.user.id;   // your own session, as before
  render(data.subscription);                // level, name, expiresAt, hwid, paused, active
} else if (data.code === 'LICENSE_NOT_ACTIVATED') {
  // Valid key, never used in the app yet. data.license has status/level.
  render('Key valid — launch the app once to activate it.');
} else {
  render('Invalid license key');            // 401, or 403 if banned
}

Once you have data.user.id, HWID reset, subscription lookups and issue-session-token SSO all work exactly as they do for user/password apps. HWID reset and ban also accept a raw license key in place of the license id, if you would rather not resolve the user first.

Do not build a key login on GET /licenses?search=. That parameter is a substring match intended for dashboard search — using it as a login lets someone probe partial keys. /licenses/authenticate matches the whole key only and is rate-limited per key.

Password reset

Evora issues and verifies; you deliver.There is no "forgot password" email sent from our side — your customers are yours, and most of them have no email address on file anyway. Instead you mint a single-use token and send it over whatever channel you already use (a Discord DM, your own mail provider, a link on your panel), then fulfil it.

1. Mint a token (your bot or panel backend)
const r = await fetch(`${API}/users/${userId}/password-reset`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ ttlSeconds: 1800 }),   // 5 min – 24 h, default 30 min
});
const { reset_token, expires_at } = await r.json();

// Deliver it yourself — it is NOT retrievable again.
await discord.users.send(discordId,
  `Reset link: https://lunar.com/reset?token=${reset_token} (expires ${expires_at})`);
2. Fulfil it when they submit a new password
await fetch(`${API}/password-reset/fulfil`, {
  method: 'POST', headers: H,
  body: JSON.stringify({ token, newPassword }),
});
// -> { success: true, userId, username, sessions_terminated }
PropertyBehaviour
StorageOnly a SHA-256 hash is stored — a database leak is not replayable
Single useFulfilling marks it used; a replay returns INVALID_TOKEN
SiblingsFulfilling (or any password change) voids every other outstanding token for that user
SessionsA successful reset kills the user's live SDK sessions
TenancyA token can only be fulfilled by the developer who owns the user
LimitsMax 3 live tokens per user; 5 issues/min per user; expired/used/unknown all return the same error
Because you identify the user before minting (by userId, not by an email form), this endpoint is not a user-enumeration oracle. Keep it that way in your panel: respond identically whether or not the account exists.

Freezing a subscription

Freezing banks the remaining time and stops the clock; resuming converts the banked seconds back into a fresh expiry. Enable allow_subscription_pause on the application first, or the pause call returns PAUSE_NOT_ENABLED.

MethodPathDescription
POST/users/:userId/subscriptions/:appId/pauseFreeze — banks remaining seconds
POST/users/:userId/subscriptions/:appId/unpauseResume — restores banked time
POST/apps/:appId/licenses/:licenseId/pauseFreeze a single license key
POST/apps/:appId/licenses/:licenseId/unpauseResume a single license key

Resuming is intentionally not gated on the app flag, so turning the feature off never strands customers who are already frozen. A paused subscription reports active: false from both authenticate endpoints, matching what the SDK does.

Self-service actions

Panel featureEndpoint
Redeem a keyPOST /users/:userId/redeem
Show subscriptions (incl. lapsed)GET /users/:userId/subscriptions?includeExpired=true
Reset HWID (cooldown enforced)POST /users/:userId/reset-hwid
Reset device bindingPOST /users/:userId/reset-device
Change passwordPUT /users/:userId

Applications

Scopes: apps:read / apps:write

MethodPathDescription
GET/appsList your applications
GET/apps/:appIdGet application details
POST/appsCreate application
PUT/apps/:appIdUpdate application
DELETE/apps/:appIdDelete application
GET/apps/:appId/statsGet app statistics

Two fields on PUT /apps/:appId govern the end-user account surface:

FieldValuesMeaning
twofa_policydisabled | optional | requiredWhether your users may (or must) enrol a second factor. Defaults to optional.
stats_publictrue | falsePublishes user/licence/online counts to the SDK via FetchStats. Defaults to false.
stats_public is a disclosure, not a display setting. It puts your user and licence totals inside a binary you have shipped to the public. Turn it on only if those numbers are ones you would publish.

Statistics

Scope: stats:read

MethodPathDescription
GET/apps/:appId/stats/overviewExtended overview (users, licenses, sessions, auth trends)
GET/apps/:appId/stats/loginsLogin activity by day (?days=30)
GET/apps/:appId/stats/usersUser growth over time (?days=30)
GET/apps/:appId/stats/licensesLicense status breakdown
GET/apps/:appId/stats/sessionsSession trends (?days=7)

Users

Scopes: users:read / users:write

MethodPathDescription
POST/apps/:appId/users/authenticateVerify user credentials (panel login)
GET/apps/:appId/users/lookup?username=Look up user by username
GET/usersList users (paginated, ?appId= filter)
GET/users/:userIdGet single user
POST/usersCreate user (username + password required)
PUT/users/:userIdUpdate user (username, email, password)
DELETE/users/:userIdDelete user
POST/users/:userId/banBan user (optional HWID/IP blacklist cascade)
POST/users/:userId/unbanUnban user
POST/users/:userId/reset-hwidReset HWID (cooldown enforced)
POST/users/:userId/reset-deviceReset device binding
GET/users/:userId/2faTwo-factor state (enabled, backup codes left, lockout)
DELETE/users/:userId/2faSupport reset — clears 2FA and ends live sessions
POST/users/:userId/redeemRedeem a license key for this user (licenses:write)
POST/users/:userId/password-resetMint a single-use reset token (you deliver it)
POST/password-reset/fulfilConsume a reset token and set the new password
POST/users/:userId/subscriptions/:appId/pauseFreeze a subscription (banks remaining time)
POST/users/:userId/subscriptions/:appId/unpauseResume a frozen subscription
POST/users/:userId/issue-session-tokenMint a one-time SDK login token (SSO)
GET/users/:userId/subscriptionsList subscriptions (?includeExpired=true)
POST/users/:userId/subscriptionsAdd subscription
POST/users/:userId/subscriptions/extendExtend subscription
DELETE/users/:userId/subscriptions/:appIdRemove subscription
POST/users/bulkBulk ban/unban/delete/reset-hwid/extend
DELETE /users/:userId/2fa is the only way to remove a second factor without presenting a code. That is why it lives on your developer key rather than the SDK — a self-service "turn it off" would defeat the feature. Use it when a customer has lost both their authenticator and their backup codes. Their live sessions are ended with the reset, on the assumption the reason may have been a compromise.
Usernames are lowercased and must use only letters, numbers, underscore, hyphen, and period — the same rules the SDK applies at login. A password of at least 6 characters is required; there is no way for an end-user to set one later on their own.
On an app with auth_mode: license, granting a subscription directly is rejected with AUTH_MODE_REQUIRES_LICENSE. The SDK reaches those customers only through a redeemed key, so a keyless grant would create an account nobody could ever log in as — use POST /users/:userId/redeem instead.

Licenses

Scopes: licenses:read / licenses:write

MethodPathDescription
GET/apps/:appId/licensesList licenses (paginated, searchable)
GET/apps/:appId/licenses/:licenseIdGet single license
POST/apps/:appId/licensesGenerate licenses
PUT/apps/:appId/licenses/:licenseIdUpdate license
DELETE/apps/:appId/licenses/:licenseIdDelete license
POST/apps/:appId/licenses/:id/banBan license
POST/apps/:appId/licenses/:id/unbanUnban license
POST/apps/:appId/licenses/:id/reset-hwidReset license HWID
POST/apps/:appId/licenses/:keyOrId/expiryExtend or reduce expiry by a signed delta (takes the raw key)
POST/apps/:appId/licenses/bulkBulk actions — see below
POST/apps/:appId/licenses/authenticateVerify a key and resolve its customer (panel login)

Bulk actions

This endpoint performs actions on existing licenses; it does not create them (to generate many at once, use amount on POST /licenses). Send an action plus its required fields. Unknown actions are rejected with 400.

cpp
# ban specific licenses
{ "action": "ban_selected", "ids": ["uuid", "..."], "reason": "chargeback" }

# extend every license in the app by 7 days
{ "action": "extend_all", "durationSeconds": 604800, "sourceFilter": "all" }
ActionRequires
delete_selected / ban_selected / unban_selectedids[] (reason optional for ban)
pause_selected / unpause_selected / reset_hwid_selectedids[]
extend_selectedids[], durationSeconds
delete_unused / delete_all—
add_timedurationSeconds (applies to unused keys)
ban_all / unban_all / pause_all / unpause_all / reset_hwid_all / delete_all_matchingsourceFilter: all | developer | reseller
extend_alldurationSeconds, sourceFilter

Variables

Scopes: variables:read / variables:write

MethodPathDescription
GET/apps/:appId/variablesList app variables
GET/apps/:appId/variables/:keyGet one variable by key
POST/apps/:appId/variablesCreate or update a variable (upsert)
PUT/apps/:appId/variables/:keyUpdate variable by key
DELETE/apps/:appId/variables/:keyDelete variable by key
DELETE/apps/:appId/variablesDelete all app variables
GET/apps/:appId/user-variablesList every user variable in the app
GET/apps/:appId/users/:userId/variablesList one user's variables
POST/apps/:appId/users/:userId/variablesSet a user variable
DELETE/apps/:appId/users/:userId/variables/:varKeyDelete a user variable
DELETE/apps/:appId/users/:userId/variablesDelete all of a user's variables
Variables are addressed by their var_key, not by an id. PUT is an upsert, so writing to an unknown key creates it.

Webhooks

Scopes: webhooks:read / webhooks:write

MethodPathDescription
GET/apps/:appId/webhooksList webhooks
POST/apps/:appId/webhooksCreate webhook
PUT/apps/:appId/webhooks/:webhookIdUpdate webhook
DELETE/apps/:appId/webhooks/:webhookIdDelete webhook
POST/apps/:appId/webhooks/:webhookId/testTest webhook
GET/apps/:appId/eventsRead the event stream (catch-up, logs:read)

Reseller account-sharing

seller.sharing_detectedfires when one of your resellers' panel accounts first looks like it is being driven by more than one person. Three independent signals feed it, so no single evasion defeats detection: distinct device fingerprints (a VPN changes the IP, not the GPU), geographic dispersion between those devices, and distinct panel sessions live on distinct networks at the same moment.

seller.sharing_detected
{
  "event": "seller.sharing_detected",
  "alert_id": "…uuid…",
  "seller": { "id": "…uuid…", "username": "reseller42" },
  "severity": "high",                       // low | medium | high | critical
  "signals": {
    "device_spread": true,                  // 3+ distinct machines in 14 days
    "geo_dispersion": true,                 // 2+ locations, 150km+ apart
    "concurrent_now": true                  // live sessions on 2+ networks
  },
  "device_count": 4,
  "location_count": 2,
  "max_distance_km": 912,
  "concurrent": true,
  "locations": [
    { "city": "Warsaw", "country": "PL", "lat": 52.23, "lon": 21.01,
      "device_count": 2, "ip_prefixes": ["31.0.34.0/24"],
      "login_count": 18, "first_seen": "…", "last_seen": "…" }
  ],
  "window_days": 14,
  "timestamp": "2026-08-07T10:12:44.201Z"
}
It fires once per alert, not once per login. A shared account logs in constantly; re-notifying on every login would be a delivery flood rather than a signal. The alert is refreshed in place as evidence accumulates, and you read the current state from GET /apps/:appId/seller-sharing-alerts and resolve it with POST /apps/:appId/seller-sharing-alerts/:alertId/review (reviewed, confirmed or dismissed).
Treat it as evidence, not a verdict.Two devices in two cities is also what a reseller with a laptop and a phone on holiday looks like. It never blocks a reseller's login, and it deliberately cannot see sharing inside one household — two people on the same home network collapse to one location and one network by design, because the alternative false-positives on every family that owns two computers.

Verify every payload

If the webhook has a secret, each request carries X-Evora-Timestamp and X-Evora-Signature. An endpoint that doesn't check them can be driven by anyone who learns its URL — a forged license.used is all it takes to make a bot grant access.

Node — verify before trusting anything
import crypto from 'node:crypto';

// The RAW body, before JSON.parse — re-serializing changes the bytes and the
// signature will never match.
app.post('/hooks/evora', express.raw({ type: 'application/json' }), (req, res) => {
  const ts  = req.header('X-Evora-Timestamp') ?? '';
  const sig = req.header('X-Evora-Signature') ?? '';

  const expected = crypto.createHmac('sha256', process.env.EVORA_WEBHOOK_SECRET)
    .update(ts).update('.').update(req.body)
    .digest('hex');

  const ok = sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  // Reject replays of an old, validly-signed request.
  if (Math.abs(Date.now() - Date.parse(ts)) > 5 * 60_000) return res.status(401).end();

  const event = JSON.parse(req.body.toString());

  // Retries reuse the same id — record it and ignore repeats, or a redelivery
  // grants twice.
  if (alreadyProcessed(req.header('X-Evora-Event-Id'))) return res.status(200).end();

  handle(event);
  res.status(200).end();   // anything non-2xx is treated as a failure and retried
});

Delivery guarantees

Delivery is at-least-once. A non-2xx response is retried with exponential backoff — 6 attempts over roughly two hours — after which the event stays in the stream but is not retried again. An endpoint that keeps failing is auto-disabled and you are notified.

The retry window is short on purpose. Enforcement is pull-based — the SDK re-reads state from the database on every heartbeat — so a missed webhook can never grant or extend access. For anything longer than a brief outage, use the event stream below; that is the durable path.

Catching up with the event stream

Every event is recorded whether or not a webhook is configured. Rather than reconciling by walking all your users on a timer, poll the cursor and get only what changed.

Catch up after downtime
let cursor = loadCursor();          // persist this

const r = await fetch(
  `${API}/apps/${APP_ID}/events?since=${cursor ?? ''}&limit=200`,
  { headers: H },
).then(r => r.json());

for (const e of r.events) applyToPortal(e);   // e.type, e.payload, e.seq
saveCursor(r.next_cursor);                    // resume here next time

Order is by seq, a monotonic integer. Don't page by timestamp — two events can share a millisecond, and a clock adjustment can make a time-based cursor skip or repeat rows.

Events

EventFires when
user.registerAn end-user registers through the SDK
user.loginAn end-user authenticates
user.banned / user.unbannedYou ban or reinstate a customer
license.usedA key is redeemed — from any surface; check `source`
subscription.createdA customer gains a subscription (key or API grant)
subscription.extendedTime is added to an existing subscription
subscription.paused / .resumedA subscription is frozen or resumed
subscription.removedA subscription is revoked
hwid.resetA customer's hardware binding is cleared
blacklist.blockedA blacklisted HWID/IP/username is refused
anti_debug / anti_vm / anti_hv / anti_http_debug / anti_attach .detectedSDK protection triggers
There is deliberately no subscription.expired. Expiry isn't an action anything performs — it's expires_at passing while nobody is looking. Derive it from the expiry field, or read subscription.active from either authenticate endpoint, which applies exactly the rule the SDK does.

Blacklist & Whitelist (API)

Scopes: apps:read / apps:write

MethodPathDescription
GET/apps/:appId/blacklistList blacklist entries
POST/apps/:appId/blacklistAdd blacklist entry
DELETE/apps/:appId/blacklist/:entryIdRemove blacklist entry
GET/apps/:appId/whitelistList whitelist entries
POST/apps/:appId/whitelistAdd whitelist entry
DELETE/apps/:appId/whitelist/:entryIdRemove whitelist entry

Sellers (API)

Scopes: sellers:read / sellers:write

MethodPathDescription
GET/apps/:appId/sellersList sellers
GET/apps/:appId/sellers/:sellerIdGet seller
POST/apps/:appId/sellersCreate seller
PUT/apps/:appId/sellers/:sellerIdUpdate seller
POST/apps/:appId/sellers/:sellerId/balanceAdd balance
DELETE/apps/:appId/sellers/:sellerIdDelete seller

Logs & Sessions (API)

Scopes: logs:read / apps:read (sessions)

MethodPathDescription
GET/apps/:appId/logsList logs (filter by type, user, date)
GET/apps/:appId/logs/statsLog statistics
GET/apps/:appId/sessionsList active sessions
DELETE/apps/:appId/sessions/:sessionIdKill session

Subscription Tiers (API)

Scopes: apps:read / apps:write

MethodPathDescription
GET/apps/:appId/subscriptionsList subscription tiers
POST/apps/:appId/subscriptionsCreate subscription tier
PUT/apps/:appId/subscriptions/:idUpdate subscription tier
DELETE/apps/:appId/subscriptions/:idDelete subscription tier

Entitlements, Geo & Floating

Scopes: entitlements:read/write · geo:read/write · floating:read/write

MethodPathDescription
GET/apps/:appId/entitlementsList entitlements
POST/apps/:appId/entitlementsCreate entitlement
PUT/apps/:appId/entitlements/:entitlementIdUpdate entitlement
DELETE/apps/:appId/entitlements/:entitlementIdDelete entitlement
GET/apps/:appId/subscriptions/:subscriptionId/entitlementsEntitlements on a tier
POST/apps/:appId/subscriptions/:subscriptionId/entitlementsAttach entitlement to a tier
GET/apps/:appId/users/:userId/entitlementsResolve a user's entitlements
GET/apps/:appId/geo-rulesList geo rules
POST/apps/:appId/geo-rulesAdd geo rule
DELETE/apps/:appId/geo-rules/:ruleIdRemove geo rule
PUT/apps/:appId/geo-enabledEnable/disable geo restrictions
GET/apps/:appId/floating/leasesList floating leases
DELETE/apps/:appId/floating/leases/:leaseIdRevoke a lease
GET/apps/:appId/users/:userId/floating/seatsSeat usage for a user

Clients & Sellers

Scopes: sellers:read / sellers:write

Clients are sub-accounts you grant management access to specific apps. They share the sellers:* scopes with reseller endpoints.

MethodPathDescription
GET/clientsList clients
POST/clientsCreate client
PUT/clients/:clientIdUpdate client
DELETE/clients/:clientIdDelete client
GET/clients/:clientId/appsList a client's app access
POST/clients/:clientId/appsGrant app access
DELETE/clients/:clientId/apps/:appIdRevoke app access

Quota

Scope: apps:read

MethodPathDescription
GET/quotaPlan quota, current usage, and subscription state

Returns quota, usage, limits (apps, clients, requests per day/minute) and subscription (expiry, days remaining, grace period). Poll this instead of guessing why a 402 or 429 appeared.

Reseller API

HTTP API at https://api.evora.lol/api/reseller-api for resellers, so a shop, Discord bot or fulfilment worker can mint and manage licenses without a browser session. Keys are minted at the moment of sale and charged against your credit balance, exactly as if you had generated them in the panel.

This exists to replace stocking by hand. Instead of bulk-generating keys in the panel and pasting them into your shop's stock, point your shop's delivery webhook at this API and it mints one on each order.

Base URL

cpp
https://api.evora.lol/api/reseller-api

Authentication

Bearer tokens prefixed ag_rk_, created under Reseller API in your resell panel. The full key is shown once at creation and stored hashed, so if you lose it, revoke it and make another.

cpp
curl -H "Authorization: Bearer ag_rk_YOUR_KEY" \
  https://api.evora.lol/api/reseller-api/me
Server-side only. An ag_rk_ key can spend your credit balance. It must never reach a browser, a client-side script, or anything you ship to a customer. If your shop platform cannot keep a secret, put a small backend in front of it.

Scopes

Each key carries its own scopes. Grant the least an integration needs: a shop that only delivers keys never needs to ban or delete them.

ScopeGrants
licenses:generateMint new keys. This is what a shop integration needs.
licenses:readList and look up keys you own.
licenses:manageReset HWID, ban, unban, delete. Only for something that handles support.
balance:readRead remaining credit per duration.
webhooks:readList webhook endpoints.
webhooks:writeCreate, edit, delete and test webhook endpoints.
API keys cannot create API keys. Key management lives in the panel under session auth only, so a leaked key cannot issue itself a broader one, and revoking it genuinely stops the integration.

IP allowlist

A key can be pinned to one or more addresses or CIDR ranges when you create it. If your shop runs on a fixed host, this turns a leaked key into a dead key. Requests from anywhere else get 403 ip_not_allowed.

Rate limits

240 requests/minute per key, plus an optional slower per-key limit you can set yourself when handing a key to a third-party integration. Over either, you get 429 with retry_after in seconds.


Idempotency

POST /licenses requires an Idempotency-Key header. It is not optional, because this endpoint gets called at the moment money changes hands by a machine that will retry on timeout. Without it, a response your shop never received means the reseller is charged twice and one batch of keys is minted into the void.

Use any string unique to the order, and reuse the same one on retries. Your shop's order id is the natural choice.

SituationResponse
First call with this keyMints normally, and the response is stored.
Retry, same key, same bodyReplays the original response with an Idempotent-Replay: true header. Nothing is minted, nothing is charged.
Retry while the first is still running409 idempotency_in_progress. Retry in a moment. If the original never finished, a retry after 5 minutes takes it over rather than waiting forever.
Same key, different body409 idempotency_key_reused. That is a client bug, so it is surfaced rather than hidden behind a replay.
Retry after a failureFailures are not cached, so a call that hit insufficient_balance can be retried against the same key once you top up.
Stored responses expire after 24 hours. Beyond that a repeated key is treated as a fresh request, so do not rely on it as a permanent dedupe log.

Minting keys

cpp
curl -X POST https://api.evora.lol/api/reseller-api/licenses \
  -H "Authorization: Bearer ag_rk_your_key" \
  -H "Idempotency-Key: order_10432" \
  -H "Content-Type: application/json" \
  -d '{"amount": 1, "duration": 1, "expiry": "month", "level": 1}'
200 OK
{
  "success": true,
  "count": 1,
  "keys": ["A1B2C-D3E4F-G5H6I-J7K8L-M9N0P"],
  "cost": { "unit": "month", "credits": 1 }
}

Request body

FieldDefaultMeaning
amount1How many keys to mint, 1 to 100.
expirydayWhich credit bucket to spend, and the unit duration is counted in: hour, day, week, month, 3month, 6month, year, lifetime.
duration1How many of that unit each key lasts.
level1Subscription level. Must be one your developer allows — see allowed_levels on GET /me.
mask*****-*****-*****-*****-*****Key format. Each * becomes a random character.
notenullFree text stored against the keys, up to 255 chars. Handy for your order id.
uppercase / lowercasetrue / falseCharacter case of generated keys.

What a mint costs

Credits are metered by the access a mint creates, not by the number of keys:

rating rule
cost = amount * duration     // charged to the bucket named by "expiry"

So thirty separate one-day keys and one thirty-day key both cost thirty day-credits, because they grant the same thirty days of access. A single one-month key on a month bucket costs one credit, which is the common case and the panel default.

RequestCost
amount 1, duration 1, expiry month1 month credit
amount 10, duration 1, expiry month10 month credits
amount 1, duration 30, expiry day30 day credits
amount 5, duration 24, expiry hour120 hour credits

duration is capped per unit (8760 hours, 3650 days, 520 weeks, 120 months, 40 quarters, 20 half-years, 10 years, 1 lifetime). Over the cap you get 400 duration_too_long rather than a confusing balance error.

Quoting before you sell

POST /licenses/quote takes the same body and prices it without minting anything, so your shop can decide whether it can fulfil an order beforetaking the customer's money.

POST /licenses/quote
{
  "quote": {
    "unit": "day", "amount": 1, "duration": 30,
    "cost": 30, "available": 120, "affordable": true,
    "durationSeconds": 2592000
  }
}
Check funding before you list a product, not after an order. A mint you cannot afford returns 402 insufficient_balance with required and available in the body — but by then your customer has already paid. Quote up front, and subscribe to balance.low so restocking is prompted rather than discovered.

Reading keys back

cpp
# paginated list of your own keys
curl "https://api.evora.lol/api/reseller-api/licenses?page=1&limit=50&search=order_10432" \
  -H "Authorization: Bearer ag_rk_your_key"

# a single key by id, for order lookups
curl https://api.evora.lol/api/reseller-api/licenses/LICENSE_ID \
  -H "Authorization: Bearer ag_rk_your_key"

Balance

Returned keyed by the same unit names POST /licenses accepts, so you can map stock to product without translating.

GET /balance
{
  "balance": {
    "hour": 0, "day": 120, "week": 40, "month": 65,
    "3month": 12, "6month": 4, "year": 2, "lifetime": 0
  },
  "threshold": 5
}

Support actions

These need licenses:manage, deliberately separate from licenses:generate. HWID reset is the highest-volume request an end user makes, so letting your bot handle it takes you out of the loop entirely.

cpp
API=https://api.evora.lol/api/reseller-api
H="Authorization: Bearer ag_rk_your_key"

curl -X POST "$API/licenses/$ID/hwid-reset" -H "$H"
curl -X POST "$API/licenses/$ID/ban"   -H "$H" -H "Content-Type: application/json" -d '{"reason":"chargeback"}'
curl -X POST "$API/licenses/$ID/unban" -H "$H"
curl -X DELETE "$API/licenses/$ID"     -H "$H"

Endpoint reference

Method & pathScope
GET /menone — any valid key
GET /balancebalance:read
POST /licenseslicenses:generate
POST /licenses/quotebalance:read
GET /licenseslicenses:read
GET /licenses/:idlicenses:read
POST /licenses/:id/hwid-resetlicenses:manage
POST /licenses/:id/banlicenses:manage
POST /licenses/:id/unbanlicenses:manage
DELETE /licenses/:idlicenses:manage
GET /webhookswebhooks:read
POST /webhookswebhooks:write
PUT /webhooks/:idwebhooks:write
DELETE /webhooks/:idwebhooks:write
POST /webhooks/:id/testwebhooks:write
POST /webhooks/:id/rotate-secretwebhooks:write
GET /webhooks/:id/deliverieswebhooks:read

Error codes

Every error carries a stable code alongside the human-readable error. Branch on the code, never on the message.

CodeStatusMeaning
invalid_api_key401Unknown, revoked or expired key.
unauthenticated401No Authorization header and no panel session.
ip_not_allowed403Key is pinned to a different address.
insufficient_scope403Key lacks the scope this route needs.
seller_disabled403Your reseller account has been disabled by the developer.
cannot_create_licenses403Key minting is turned off for your account.
level_not_allowed403Requested level is outside the levels you may sell.
license_limit_reached403You have hit the total key cap set by the developer.
app_mismatch403Key was issued for a different application.
insufficient_balance402Not enough credit in that bucket. Body carries required and available. Top up and retry with the same Idempotency-Key.
duration_too_long400duration exceeds the cap for that unit.
idempotency_key_required400POST /licenses was called without an Idempotency-Key header.
idempotency_in_progress409An identical request is still running.
idempotency_key_reused409Key reused with a different body.
license_not_found404No such key, or it is not one of yours.
rate_limited429Over the per-key limit. Honour retry_after.

Webhooks

Evora can call your endpoint when something happens to the keys you sold. Add endpoints in the panel under Reseller API, or over the API with webhooks:write. HTTPS only.

Events

EventFires when
license.redeemedA key you sold was activated by an end user, on any surface: loader, account panel or API.
balance.lowA sale took one of your duration buckets down to your threshold.
Wire up balance.low first. It fires on the sale that crosses the threshold, not on every sale below it, so it arrives while you still have stock to sell rather than after an order has already failed. Set the threshold on the same panel screen; 0 turns the event off.

Payloads

license.redeemed
{
  "event": "license.redeemed",
  "timestamp": "2026-08-07T09:41:22.104Z",
  "license": { "id": "…uuid…", "key": "A1B2C-D3E4F-G5H6I-J7K8L-M9N0P", "level": 1 },
  "user": { "id": "…uuid…", "username": "customer42" },
  "subscription": { "name": "Premium", "expires_at": "2026-09-06T09:41:22.000Z" },
  "extended": false,
  "source": "loader"
}
balance.low
{
  "event": "balance.low",
  "timestamp": "2026-08-07T09:41:22.104Z",
  "unit": "month",
  "remaining": 4,
  "threshold": 5,
  "seller": { "id": "…uuid…", "username": "yourname" }
}

Verifying the signature

Every delivery is signed with the secret shown once when you created the endpoint. The signature is HMAC-SHA256 over timestamp + "." + rawBody. Verify against the raw body, before any JSON parsing.

HeaderContains
X-Evora-SignatureHMAC-SHA256 hex digest.
X-Evora-TimestampUnix seconds, the value signed alongside the body.
X-Evora-Event-IdStable id for this event. Retries reuse it, so record it to dedupe.
X-Evora-Delivery-Attempt1 on the first try, incrementing per retry.
Node — express
import crypto from 'node:crypto';

app.post('/evora-hook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts  = req.get('X-Evora-Timestamp');
  const sig = req.get('X-Evora-Signature');

  const expected = crypto.createHmac('sha256', process.env.EVORA_WEBHOOK_SECRET)
    .update(ts).update('.').update(req.body)
    .digest('hex');

  // timing-safe, and length-checked first so timingSafeEqual cannot throw
  const ok = sig && sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  // reject anything older than five minutes so a captured delivery cannot be replayed
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString());
  // dedupe on X-Evora-Event-Id before acting — retries reuse it
  handle(event);
  res.sendStatus(200);
});

Retries

A delivery is a success only on a 2xx. Anything else is retried at 30s, 2m, 10m, 30m and 2h, six attempts in total. After 20 consecutive failures the endpoint is disabled automatically and you will see why in the panel; re-enabling it clears the counter. Deliveries and their outcomes are listed under GET /webhooks/:id/deliveries.

Webhooks are notification, not control. If your receiver is down you have lost a message, never a key or a credit — the authoritative state is always readable from GET /licenses and GET /balance.

Recipe: automatic shop delivery

The shape below works with any shop platform that can call a URL on a paid order (Sellix, Shoppy, a custom store). Your backend holds the key; the shop only ever sees the license you hand back.

Node — order paid → deliver
const API = 'https://api.evora.lol/api/reseller-api';

async function deliver(order) {
  const res = await fetch(`${API}/licenses`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.EVORA_RESELLER_KEY}`,
      'Content-Type': 'application/json',
      // the order id IS the idempotency key. a retried webhook from the shop
      // now replays the same license instead of minting a second one.
      'Idempotency-Key': order.id,
    },
    body: JSON.stringify({
      amount: 1,
      duration: order.months,
      expiry: 'month',
      level: order.tier,
      note: `order ${order.id}`,
    }),
  });

  const body = await res.json();

  if (!res.ok) {
    if (body.code === 'insufficient_balance') {
      // body.required / body.available tell you how far short you are.
      // do NOT mark the order fulfilled — top up, then retry with the same
      // Idempotency-Key and this call becomes a normal mint.
      await alertMe(`need ${body.required} ${body.unit}, have ${body.available}`);
      return { delivered: false, retryable: true };
    }
    if (body.code === 'idempotency_in_progress') return { delivered: false, retryable: true };
    return { delivered: false, retryable: false, reason: body.code };
  }

  return { delivered: true, key: body.keys[0] };
}

Pair it with a balance.low endpoint so restocking is prompted rather than discovered, and with license.redeemed if you want to know which orders have actually been activated.

API Keys

API keys let you manage your applications programmatically from external tools, bots, CI/CD pipelines, or your own backend. Instead of using the dashboard manually, you can automate license generation, user management, variable updates, and more through the REST API.

Creating an API key

Go to API Keys in the developer dashboard. Each key has a name, an optional expiry date, and a set of scopes. The key is shown once on creation (format: ag_sk_...). Store it securely.

Scopes

ScopeAccess
apps:readList and view applications, stats, blacklist, whitelist, sessions
apps:writeCreate, update, delete apps. Manage blacklist, whitelist, sessions.
licenses:readList and view licenses for an app
licenses:writeCreate, update, delete, ban/unban licenses. Bulk operations.
users:readList and view users and their subscriptions
users:writeCreate, update, delete, ban/unban users. HWID/device reset. Bulk operations.
variables:readRead app variables and per-user variables
variables:writeCreate, update, delete app and per-user variables
webhooks:readList and view webhooks
webhooks:writeCreate, update, delete, test webhooks
sellers:readList and view seller accounts
sellers:writeCreate, update, delete sellers. Manage balances.
logs:readView application logs and log statistics
stats:readDashboard analytics: overview, login trends, user growth, aggregates
entitlements:readView entitlements and their subscription/user bindings
entitlements:writeCreate, update, delete, and attach entitlements
geo:readView geo restriction rules
geo:writeAdd, remove, and toggle geo restriction rules
floating:readView floating license leases and seat usage
floating:writeRevoke floating license leases

App-scoped keys

A key can optionally be locked to a single application. Scoped keys are rejected on any route touching another app, cannot create new applications, and see only their own app in GET /apps. Use one per integration so a leaked bot token can't reach your other products.

Rate limits

Two limits apply. A fixed anti-abuse ceiling of 300 requests/minute per key, and your plan's requests-per-minute quota, which is the one you'll actually hit — exceeding it returns 429 with a retry_after value in seconds. You can also set a lower rateLimitPerMinute on an individual key when handing it to a third party.

Using an API key

cpp
Authorization: Bearer ag_sk_your_api_key_here
Never expose API keys in client-side code. These are server-to-server credentials. If a key is compromised, revoke it immediately from the dashboard.

Subscriptions

Subscription tiers define the access levels for your application. Each tier has a name, a numeric level, and a duration. When a user authenticates, their subscription and subscription_level fields in UserData reflect their active tier.

Setting up tiers

Create tiers in the dashboard under your app's Subscriptions page, or via the REST API. Each tier needs:

FieldDescription
nameDisplay name (e.g. "Basic", "Premium", "Lifetime")
levelNumeric level. Higher means more access. Use this in your app to gate features.
durationHow long the subscription lasts (days, or lifetime)

Using tiers in your app

cpp
auto& user = client.User();

if (user.subscription_level >= 2) {
    enable_advanced_mode();
}

if (user.IsLifetime()) {
    std::cout << "lifetime access\n";
} else {
    std::cout << "expires in " << user.FormatTimeLeft() << "\n";
}

Assigning subscriptions

Users get a subscription when they redeem a license key or when you assign one manually through the dashboard or API. Licenses are linked to a subscription tier, so the tier is applied automatically on activation.


Blacklist & Whitelist

Blacklist

Block specific HWIDs or IP addresses from authenticating. Blacklisted devices get ErrorCode::UserBanned on any auth attempt. Entries can be added manually or triggered automatically by anti-debug detections (when Auto-Ban is enabled) or the abuse detection system.

Whitelist

Restrict authentication to only approved devices. When the whitelist is active for an app, only HWIDs or IPs on the list can connect. Everything else is rejected. Useful for internal testing, closed beta access, or dedicated device deployments.

SDK behavior

cpp
if (client.IsBlacklisted()) {
    std::cerr << "this device is banned\n";
    return 1;
}

auto r = client.Login(user, pass);
if (r.blacklisted) {
    std::cerr << "banned\n";
}

Sessions

Every authenticated SDK client creates a session on the server. Sessions are kept alive by heartbeats and expire when the client disconnects or stops sending heartbeats.

Managing sessions

From the dashboard or the Developer API, you can:

  • View active sessions: see every connected client with their username, IP, HWID, connection time, and last heartbeat
  • Kill a session: forcibly disconnect a specific client. If the client is on WebSocket transport, they receive a "kill" push event.
  • Kill all sessions: disconnect every active client for an app at once
  • Live stream: the dashboard includes a real-time session feed that updates as clients connect and disconnect

API endpoints

cpp
# list active sessions
GET /api/developer-api/apps/:appId/sessions

# kill a specific session
DELETE /api/developer-api/apps/:appId/sessions/:sessionId

# kill all sessions
POST /api/developer-api/apps/:appId/sessions/kill-all

SellersPro / Ultra

The seller system lets you create sub-accounts that can generate and distribute licenses on your behalf. Each seller has a balance and can only create licenses up to the number of credits you assign them.

How it works

  • Create a seller: give them a name, set their initial balance, and assign which subscription tiers they can sell
  • Seller generates licenses: each license costs 1 credit from their balance
  • Top up balance: add more credits as the seller purchases them from you
  • Track activity: view the seller's ledger (balance changes) and action log (licenses created, users managed)

API endpoints

cpp
# list sellers
GET /api/developer-api/apps/:appId/sellers

# create a seller
POST /api/developer-api/apps/:appId/sellers
  {"name": "reseller1", "balance": 100}

# add credits
POST /api/developer-api/apps/:appId/sellers/:sellerId/balance
  {"amount": 50}

Abuse DetectionPro / Ultra

The platform monitors authentication patterns and flags suspicious activity automatically. This includes things like rapid HWID changes, mass login attempts, credential sharing, and unusual geographic patterns.

Features

  • Automated scanning: the system continuously analyzes authentication logs for known abuse patterns
  • Alerts: suspicious activity generates alerts you can review in the dashboard or pull via the API
  • Configurable settings: tune detection thresholds per-app (e.g. how many HWID resets before flagging, login rate limits)
  • Manual scans: trigger a full abuse scan on demand from the dashboard

Review alerts and take action (ban, reset HWID, etc.) directly from the alert detail view. The system flags suspicious users but doesn't auto-ban unless you configure it to.


Utility Methods

Helper methods available on the Client instance after construction:

MethodReturnsDescription
Initialized()boolWhether Init() completed successfully
Authenticated()boolWhether Login/Register/License succeeded
LastError()std::stringHuman-readable last error message
LastErrorCode()ErrorCodeEnum value of last error
GetSdkVersion()std::stringSDK version string (e.g. "2.9.7")
GetAuthMode()std::stringApp's configured auth mode ("license", "user_pass", "both")
GetAppName()std::stringApplication name from the dashboard
IsBlacklisted()boolWhether current HWID is blacklisted
IsWebSocketConnected()boolWhether the WebSocket connection is active
User()const UserData&Authenticated user's data
Wait()voidBlocks until Close() is called or the process exits
Close()voidShuts down all background threads and disconnects
cpp
if (!client.Initialized()) {
    std::cerr << "Init failed: " << client.LastError() << "\n";
    return 1;
}

std::cout << "SDK v" << client.GetSdkVersion() << "\n";
std::cout << "App: " << client.GetAppName() << "\n";
std::cout << "Auth mode: " << client.GetAuthMode() << "\n";

client.Wait();

Preprocessor Defines

Optional defines you can set before including Evorion.h to customize SDK behavior:

DefineEffect
EVORION_SDK_VERSIONDefined automatically by the header. Use for compile-time version checks.
EVORION_NO_AUTOLINKDisables the #pragma comment(lib, ...) directives. Define this if you link system libraries manually or use a custom build system.
EVORION_NO_ANTIDEBUGStrips all anti-debug code at compile time. Useful for debug builds where you need to attach a debugger.
EVORION_NO_PROTECTStrips all code protection macros (PROTECT, PROTECT_SEH, SPLIT, FLOW) so they compile to nothing.
EVORION_NO_TLS_CALLBACKSDisables TLS callback registration. Define this if you handle TLS callbacks yourself or use a packer that conflicts with them.
cpp
#ifdef _DEBUG
#define EVORION_NO_ANTIDEBUG
#endif

#define EVORION_NO_AUTOLINK

#include "Evorion.h"

When auto-linking is enabled (default), the SDK automatically links: winhttp, crypt32, bcrypt, wbemuuid, gdiplus, ole32, ws2_32, iphlpapi.


Full Example

A complete integration with auth mode routing, file download, and user data access:

main.cpp — full integration
#include "Evorion.h"
#include <iostream>
#include <fstream>

int main() {
    EVORION_CLIENT(client,
        "your-owner-uuid",
        "your-app-uuid",
        "1.0",
        evorion::TransportMode::WebSocket,
        true, 30, 500, true
    );

    if (!client.Initialized()) {
        std::cerr << "init failed: " << client.LastError() << "\n";
        return 1;
    }

    client.OnPush([](const std::string& type, const std::string& payload) {
        if (type == "kill" || type == "ban")
            ExitProcess(0);
    });

    std::string mode = client.GetAuthMode();
    evorion::Result auth;

    if (mode == "license") {
        std::string key;
        std::cout << "License key: ";
        std::getline(std::cin, key);
        auth = client.License(key);
    } else {
        std::string user, pass;
        std::cout << "Username: "; std::getline(std::cin, user);
        std::cout << "Password: "; std::getline(std::cin, pass);
        auth = client.Login(user, pass);
    }

    if (!auth.ok()) {
        std::cerr << auth.message() << "\n";
        return 1;
    }

    EVORION_AUTH_PROTECT(client, post_auth)
        auto& u = client.User();
        std::cout << "Welcome " << u.username << " (" << u.subscription << ")\n";
        std::cout << "Time left: " << u.FormatTimeLeft() << "\n";

        auto file = client.DownloadFile("config-file-uuid");
        if (file.ok()) {
            auto bytes = file.FileContents();
            std::ofstream out(file.FileName(), std::ios::binary);
            out.write((const char*)bytes.data(), bytes.size());
            std::cout << "saved " << file.FileName() << "\n";
        }
    EVORION_PROTECT_DONE(post_auth)

    EVORION_SHIELD_INIT(client);

    EVORION_ENCRYPT_BEGIN;
        std::cout << "this code was encrypted on disk and decrypted via server\n";
    EVORION_ENCRYPT_END;

    EVORION_SHIELD_SHUTDOWN(client);

    client.Wait();
}

Macros

MacroPurpose
EVSK("str")Compile-time encrypted SecureCredential
EVORION_CLIENT(var, owner, app, ver, ...)Declares a Client with an EVSK-encrypted owner id
EVORION_AUTH_PROTECT(client, name) / PROTECT_DONE(name)Auth-verified block: _evr_vc gate at entry, fail-closed on invalid session
EVORION_LOCKED_INT / _UINT / _INT64(client, name, value)Auth-gated constant. .load() returns zero on invalid session
EVORION_ENCRYPT_BEGIN / ENDByte-marker for post-build code encryption via evora-protect CLI
EVORION_SHIELD_INIT(client)Initialize auto-decrypt for encrypted code sections (call after auth)
EVORION_SHIELD_SHUTDOWN(client)Clean up shield runtime
EVORION_DYNAPI(dll, api)Dynamic API resolution via PEB walk (no IAT entry)
EVORION_REQUIRE_AUTH(client)Inline check — Client::Authenticated()
sscx (function attribute)Opts function into MAX-tier server-side shield-VM lift
SecureStr("str")Compile-time + runtime encrypted string
WSecureStr(L"str")Wide-char compile-time + runtime encrypted string

Changelog

v3.1.0 Latest

  • Renamed plan tiers in dashboard: Platinum is now Pro, Obsidian is now Ultra (no schema changes — your existing licenses, API keys, and SDK behavior are unaffected)
  • Unified security panel on the developer dashboard — single coverage view instead of per-protection breakdown
  • Fixed broadcast notification delivery (developer-wide notices)
  • Fixed sparkline chart rendering at line endpoints
  • Pie charts now render an empty placeholder when underlying data has no recent activity
  • Theme toggle no longer renders twice on the landing page
  • Security cap UI is correctly suppressed for Ultra tier (unlimited)

v2.9.8

  • Added Developer REST API with scoped API keys
  • Added seller/reseller system with balance tracking
  • Added abuse detection and alerting
  • Added subscription tier management via API
  • Runtime protection macros overhauled — see Code Protection section for current primitives (EVORION_AUTH_PROTECT, EVORION_LOCKED_INT, EVORION_ENCRYPT_BEGIN/END, EVORION_DYNAPI).

v2.9.7

  • Added DownloadFile(file_id) for encrypted file delivery
  • Added Result::FileContents() and Result::FileName()
  • Improved anti-tamper mesh integrity checks
  • Performance: parallelized security checks on init

v2.9.5

  • WebSocket transport mode with server push support
  • Capability-token based authorization flow
  • DPoP proof-of-possession for request signing
  • Remote attestation challenge/response system

v2.9.0

  • Auth mode enforcement (license, user_pass, both)
  • Automatic heartbeat and anti-debug via constructor
  • SecureCredential and EVSK() macro for compile-time encryption

API reference

Every endpoint below is generated from the OpenAPI specification, so it always matches what the server actually implements — 109 operations in total. A drift check fails the build if a route and the specification disagree.

Base URL

https://api.evora.lol/api/developer-api

Machine-readable

Import the specification into Postman, Insomnia, Apidog or Scalar, or use it to generate a client library. Agents should start at llms.txt.

openapi/developer-api.yamlOpenAPI 3.1, the source of truth
openapi/developer-api.jsonthe same specification as JSON
llms.txtorientation for AI agents

All 109 endpoints, grouped. Each has its own page with parameters, response body and required scope — browse the full reference.

Quota

  • getPlan quota and current usage

Applications

  • getList applications
  • postCreate an application
  • getGet an application
  • putUpdate an application
  • deleteDelete an application

Statistics

  • getApplication statistics
  • getExtended overview
  • getLogin activity by day
  • getUser growth over time
  • getLicense status breakdown
  • getSession trends

Licenses

  • getList licenses
  • postGenerate licenses
  • getGet a license
  • putUpdate a license
  • deleteDelete a license
  • postBan a license
  • postUnban a license
  • postReset a license HWID
  • postFreeze a license
  • postResume a frozen license
  • postAdjust license expiry by a signed delta
  • postBulk license actions

Users

  • getLook up a customer by username
  • getList customers
  • postCreate a customer
  • getGet a customer
  • putUpdate a customer
  • deleteDelete a customer
  • postMint a password-reset token
  • postConsume a reset token and set the new password
  • postBan a customer
  • postUnban a customer
  • postReset a customer's HWID
  • getRead a customer's two-factor state
  • deleteReset a customer's two-factor authentication
  • postReset a customer's device binding
  • postMint a one-time SDK login token (panel SSO)
  • postRedeem a license key on a customer's behalf
  • postBulk customer actions

Authentication

  • postAuthenticate a customer by license key
  • postAuthenticate a customer by username and password

Subscriptions

  • getList a customer's subscriptions
  • postGrant a subscription directly
  • deleteRemove a subscription
  • postFreeze a subscription
  • postResume a frozen subscription
  • postExtend a subscription by days

Subscription tiers

  • getList tiers
  • postCreate a tier
  • putUpdate a tier
  • deleteDelete a tier

Variables

  • getList app variables
  • postCreate or update a variable
  • deleteDelete all app variables
  • getGet a variable
  • putUpdate a variable
  • deleteDelete a variable
  • getList every user variable in the application
  • getList one customer's variables
  • postSet a customer variable
  • deleteDelete all of a customer's variables
  • deleteDelete a customer variable

Webhooks

  • getList webhooks
  • postCreate a webhook
  • getGet a webhook
  • putUpdate a webhook
  • deleteDelete a webhook
  • postFire a test delivery
  • getRead the event stream (catch-up)

Access control

  • getList blacklist entries
  • postAdd a blacklist entry
  • deleteRemove a blacklist entry
  • getList whitelist entries
  • postAdd a whitelist entry
  • deleteRemove a whitelist entry

Sessions

  • getList live sessions
  • deleteKill a session
  • postKill every live session

Logs

  • getList logs
  • getLog statistics

Sellers

  • getList sellers
  • postCreate a seller
  • getGet a seller
  • putUpdate a seller
  • deleteDelete a seller
  • postAdd seller balance

Clients

  • getList clients
  • postCreate a client
  • putUpdate a client
  • deleteDelete a client
  • getList a client's application access
  • postGrant application access
  • deleteRevoke application access

Entitlements

  • getList entitlements
  • postCreate an entitlement
  • putUpdate an entitlement
  • deleteDelete an entitlement
  • getList entitlements attached to a tier
  • postAttach an entitlement to a tier
  • getResolve a customer's entitlements

Geo

  • getList geo rules
  • postAdd a geo rule
  • deleteRemove a geo rule
  • putEnable or disable geo restrictions

Floating licenses

  • getList active leases
  • deleteRevoke a lease
  • getSeat usage for a customer