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.
Installation
- Download the SDK— Grab the latest release from the Evora dashboard. You'll get
Evorion.handEvorion.lib. - Drop into your project — Place both files alongside your source. The header handles all library linking via
#pragma comment(lib, ...). - Get your credentials — In the dashboard, go to Settings → Credentials to find your App ID and Owner ID for the public SDK surface.
EVSK() macro to encrypt them at compile time.Quick Start
The simplest integration. Everything runs automatically:
#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.
- 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.
- Create an application — in the dashboard, add an app and copy the
app_id,app_secret, and (optional) public key. - Pick an authentication mode —
licensefor license-key only,user_passfor username/password, orboth. See Authentication Modes. - Drop the SDK into your project — add the headers and link the static lib. All you need is
#include <evorion/evorion.h>. - Initialize the client — instantiate
Evorion::Clientwith yourapp_id,app_secret, and version string. The constructor starts heartbeat and anti-debug automatically. - Wire your auth flow — call
Login(),Register(), orLicense()based on your chosen mode. Inspect theResultfor the outcome. - Configure version policy — set ok / warn / block rules per version in the dashboard so old clients can't bypass updates.
- Enable integrity checking — turn on Anti-Debug, Anti-VM, and Integrity for your app. The SDK will register a golden image on first connect.
- Wrap sensitive code with real protection primitives —
EVORION_ENCRYPT_BEGIN/ENDfor post-build code encryption (evora-protect CLI),EVORION_AUTH_PROTECTfor auth-gated blocks,EVORION_LOCKED_INTfor auth-gated constants,EVORION_DYNAPIto hide imports,EVSK()for compile-time string encryption, and thesscxfunction marker for MAX-tier server-side lift. - Test in transport modes you'll use — HTTPS works everywhere; WebSocket gives lower latency and server push.
- Set up sessions and ban policies — pick concurrent session limits, ban triggers, and abuse detection thresholds in the dashboard.
- Optionally use the Developer API — issue scoped API keys and automate user, license, or seller management from your own backend.
- 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
licenseauth mode and then callingLogin()— 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.
| Mode | Allowed | Blocked |
|---|---|---|
| both | Login() · Register() · License() | None |
| license | License() | Login() · Register() |
| user_pass | Login() · Register() | License() |
Calling a blocked method returns ErrorCode::AuthModeRestricted. Check the mode first to show the right UI:
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
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.
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
Creates a new user account. Does not auto-login, so call Login() afterwards. Blocked when auth mode is "license".
auto r = client.Register("newuser", "securepass"); if (r.ok()) { // now login with the new account auto login = client.Login("newuser", "securepass"); }
License Key
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.
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
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.
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
| Parameter | Description |
|---|---|
| owner_id | Your owner UUID from the dashboard |
| app_id | Application UUID |
| version | Your app version string (for version gating) |
| mode | Http or WebSocket. WebSocket enables server push. |
| auto_init | Calls Init() automatically in the constructor |
| heartbeat_interval | Heartbeat interval in seconds. Set 0 to disable. |
| antidebug_interval | Anti-debug scan interval in milliseconds. Set 0 to disable. |
| auto_exit | Terminate process on tamper detection |
Utility methods
| Method | Returns | Description |
|---|---|---|
| Initialized() | bool | Whether Init() succeeded |
| Authenticated() | bool | Whether user is logged in |
| LastError() | const string& | Last error message |
| LastErrorCode() | ErrorCode | Last typed error code |
| User() | const UserData& | Current user data (after auth) |
| IsBlacklisted() | bool | Whether this HWID is banned |
| GetAppName() | string | App name from server config |
| GetAuthMode() | string | "license", "user_pass", or "both" |
| GetSdkVersion() | string | SDK version (e.g. "2.9.7") |
| IsWebSocketConnected() | bool | WS transport connection status |
| Wait() | void | Block forever (heartbeat stays alive) |
| Close() | void | Tears down the client, stops all timers and closes transports |
Result
Returned by every API call. Check ok() first, then read details.
| Field / Method | Type | Description |
|---|---|---|
| ok() | bool | Operation succeeded |
| message() | string | Human-readable status or error |
| error_code | ErrorCode | Typed enum for programmatic handling |
| error | string | Raw error string |
| json | string | Raw JSON response body |
| blacklisted | bool | Device/user is on the blacklist |
| FileContents() | vector<uint8_t> | Binary file data (only after DownloadFile()) |
| FileName() | string | Original filename (only after DownloadFile()) |
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
| Field | Type | Description |
|---|---|---|
| username | string | Account username |
| hwid | string | Hardware ID bound to this account |
| ip | string | IP address (server-reported) |
| subscription | string | Subscription plan name |
| subscription_level | int | Numeric tier level |
| expiry | string | Expiry date/time |
| create_date | int64_t | Account creation (Unix timestamp) |
| last_login | int64_t | Last login (Unix timestamp) |
| variables | map<string,string> | Server-defined per-user key-value data |
Helper methods
| Method | Returns | Description |
|---|---|---|
| HasSubscription() | bool | Has any active subscription |
| IsLifetime() | bool | Subscription never expires |
| GetTimeLeftSeconds() | int64_t | Seconds until expiry |
| FormatTimeLeft() | string | Formatted time remaining |
| GetVariable(key, default) | string | Lookup a user variable |
| IsValid() | bool | Whether UserData has been populated |
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:
| Code | Value | Meaning |
|---|---|---|
| None | 0 | No error |
| Unknown | 1 | Unclassified |
| InvalidCredentials | 2 | Wrong username/password |
| InvalidLicense | 3 | License key not found or already used |
| HwidMismatch | 4 | Hardware doesn't match registered device |
| UserBanned | 5 | Account is banned |
| SubscriptionExpired | 6 | Subscription ran out |
| NoSubscription | 7 | No active subscription |
| DeviceMismatch | 8 | Device fingerprint mismatch |
| SessionExpired | 9 | Session token expired |
| IntegrityViolation | 10 | Binary tamper detected |
| VersionBlocked | 11 | App version is blocked |
| AuthModeRestricted | 12 | Auth mode doesn't allow this method |
| ServerError | 13 | Server-side error |
Init
Establishes a session with the server: device registration, session tokens, and app config. Only needed when auto_init is false.
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
Validates the current session is still live on the server. Useful for manual session verification outside the automatic heartbeat cycle.
auto r = client.Check(); if (!r.ok()) { // session died, re-authenticate }
Heartbeat
Keeps the session alive. Runs automatically in a background thread (controlled by heartbeat_sec), but you can call it manually if needed.
GetVar
Fetches a server-side variable by name. Value is returned in Result::json.
auto r = client.GetVar("motd"); if (r.ok()) std::cout << "Message: " << r.json << "\n";
SetUserVar New
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.
// 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
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.
auto r = client.GetUserVar("play_count"); if (r.ok()) std::cout << "plays: " << r.json << "\n";
// 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() fetches application-wide variables (same value for all users). GetUserVar() fetches per-user variables (unique to each authenticated user).FetchOnline New
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.
auto r = client.FetchOnline(); if (r.ok()) std::cout << "Users online: " << r.json << "\n"; // output: {"success":true,"online":17}
Ban New
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.
// detected something suspicious, ban and exit client.Ban("Tamper detected by client"); std::exit(1);
InvokeWebhook NewPro / Ultra
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.
auto r = client.InvokeWebhook( "wh-uuid-from-dashboard", R"({"event":"level_complete","score":9500})" ); if (!r.ok()) std::cerr << "webhook failed: " << r.message() << "\n";
client.InvokeWebhook("wh-uuid-from-dashboard");
DownloadFile New in v2.9.7
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.
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());
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
Registers a callback for server-pushed messages. Only works in WebSocket transport mode. Common push types include "kill" and "ban".
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:
| Policy | Behaviour |
|---|---|
| Disabled | Nobody can enrol. Existing enrolments are ignored at login. |
| Optional | Users may enrol. Those who have are challenged; those who have not sign in normally. This is the default. |
| Required | Sign-in is refused with TWOFA_ENROLMENT_REQUIRED until the user enrols. Users cannot turn it back off. |
Setup2FA
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.
auto setup = client.Setup2FA(password); if (setup.ok()) { // otpauth_uri -> QR, secret -> manual entry fallback ShowEnrolmentScreen(setup.json); }
Confirm2FA
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.
auto done = client.Confirm2FA(userTypedCode); if (done.ok()) { ShowBackupCodesOnce(done.json); // backup_codes[] }
Disable2FA
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
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
| Code | Meaning | What to do |
|---|---|---|
| TWOFA_REQUIRED | Credential was correct; a code is needed. | Prompt, then retry Login/License with the code. |
| TWOFA_INVALID | Code rejected. | Let the user retry. The budget is finite — see below. |
| TWOFA_LOCKED | Too many wrong codes on this account. | Show retry_after_ms and stop submitting. |
| TWOFA_ENROLMENT_REQUIRED | App policy is required; account has no second factor. | Route into Setup2FA. |
| TWOFA_NO_ENROLMENT | Confirm2FA called with no setup in progress, or it expired. | Start again from Setup2FA. |
| REAUTH_REQUIRED | Password needed for this operation. | Prompt for the password and repeat the call. |
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
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.
client.Logout(); // this device client.Logout(true); // every device
ChangeUsername
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.
| Code | Meaning |
|---|---|
| USERNAME_TAKEN | Already in use on your account, or reserved. |
| USERNAME_INVALID | Fails the username rules for this app. |
| USERNAME_COOLDOWN | Changed too recently. next_allowed_at says when. |
| USERNAME_UNCHANGED | Same as the current name. |
Password recovery
Recovery is deliberately a two-party flow: Evora issues and verifies the token, you deliver it.
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-resetand 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.
// 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
Returns users, licenses, online and versionfor the application — the numbers behind a "1,204 users online" banner.
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
.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.
| Threat | Why Evorion cannot help |
|---|---|
| Inline plaintext cheat code in your .text | If 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 binary | XOR keys, AES keys, secret salts compiled into your loader will be extracted in minutes. Treat your binary as public. |
| Plaintext URLs to payloads | Even 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 stripping | Symbols 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 symbols | WER + minidumps reveal call stacks. Strip PDB, ship with /Brepro and stripped debug info. |
| Server-side account compromise | If 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 hand | Out 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
Wrong — vulnerable to a 1-byte jnz flip
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)
#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.
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.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.
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.
[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 liftssscx-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
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/ENDfor post-build encryption,EVORION_AUTH_PROTECTfor auth-gated blocks, andEVSK()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 (
-O2at 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.lolaccount 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/CreateProcessfor 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 otherif (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 — viasession::transfer(server-issued content) orsession::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 writingif (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
.textas 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_VERBOSEdefined) to customers — log strings leak everything. - Don't compile with PDB paths reachable from the binary's
.rdata(use/PDBALTPATH:%_PDB%). - Don't ship
OutputDebugStringcalls in release builds — debuggers attach silently to read them. - Don't bundle
evr_diag.log/evr_crash.dmpin 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 / PDB | Your function names + paths land in the release binary. Reverser already has half of IDA's job done for them. |
| EVORION_VERBOSE left on in release | Log file evr_diag.log written to disk; debug-string artifacts in .rdata; failure reasons leaked to any reader. |
| TLS pinning / SessionFetch | MitM swaps your payload; you become a vector. |
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.
| Term | Meaning |
|---|---|
| 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
| Term | Meaning |
|---|---|
| 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. |
| SessionFetch | In-process TLS-pinned download. Replaces popen("curl ..."). |
| Session secret | Per-session 32-byte derived key material rotated by heartbeat. Feeds both consumer primitives. |
| HWID | Hardware ID hash derived from CPUID + SMBIOS + TPM endorsement key. License is bound to first-seen HWID by default. |
| Heartbeat | Periodic re-auth that rotates the session secret. Default 30 s. Reduce for higher-security flows. |
| Automatic protection | The 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.
| Term | Why 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_place | Removed. 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.
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.
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
- 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.
- Enable. Toggle integrity checking on for your application in Binary Integrity settings.
- Challenge. During heartbeats, the server picks random regions of .text and sends an attestation challenge with a fresh nonce.
- Proof. The SDK reads its own memory at those offsets, computes
HMAC-SHA256(regions || nonce, BUILD_SECRET), and returns the proof. - Verify. Server verifies the proof against its golden image. Mismatch →
IntegrityViolation.
Dashboard setup
Navigate to your app's Binary Integrity page. Two upload modes:
| Mode | Description |
|---|---|
| Binary Upload | Drag & drop your compiled .exe, .dll, or .sys. The server automatically extracts the .text section via its PE parser. |
| Hash Upload | Paste a pre-extracted .text section as base64. Use this if you extract the .text section yourself or from a CI pipeline. |
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.
// 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
| Property | Detail |
|---|---|
| Regions | Random .text offsets per challenge, can't precompute |
| Nonce | 32-byte fresh nonce per challenge, prevents replay |
| Algorithm | Multiple HMAC variants (0–3), defeats generic hash emulators |
| Secret | HMAC 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.
.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:
# 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
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.
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:
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:
EVORION_CLIENT(client, "owner-uuid", "app-uuid", "1.0", evorion::TransportMode::WebSocket);
SecureCredential class
| Method | Description |
|---|---|
| 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:
| Mode | Use Case |
|---|---|
| TransportMode::Http | Standard HTTPS request/response. Simpler, works behind restrictive firewalls. Default. |
| TransportMode::WebSocket | Persistent connection. Enables server push (OnPush()), remote kill/ban, real-time variable updates. |
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:
| Policy | Behavior |
|---|---|
| ok | Version is accepted. Normal operation. |
| warn | Version is outdated but allowed. Init() succeeds. The server may include an update_url in the response. |
| block | Version is rejected. Init() fails with ErrorCode::VersionBlocked. The user must update. |
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.
| Macro | What it does | When to use |
|---|---|---|
| EVORION_ENCRYPT_BEGIN / END | Byte-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. |
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
- Mark code sections with
EVORION_ENCRYPT_BEGIN/EVORION_ENCRYPT_ENDin your source. The markers compile to harmless jumps, so the code runs normally before protection. - 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.
- At runtime, call
EVORION_SHIELD_INITafter 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. - The section is decrypted in memory only. The binary on disk stays encrypted.
// after authenticating, init the shield runtime EVORION_SHIELD_INIT(client); EVORION_ENCRYPT_BEGIN; do_critical_work(); EVORION_ENCRYPT_END;
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.
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.
auto path = WSecureStr(L"C:\\secret\\config.dat"); { auto v = path.access(); load_config(v.ptr()); }
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:
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
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
| Macro | Best 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.
#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.
#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.
#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); }
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.
What you need
| Requirement | Detail |
|---|---|
| A domain you own | Any registrar — Cloudflare, Namecheap, GoDaddy, Porkbun. You do not need to move the domain to Cloudflare. |
| A spare subdomain | Use one you are not already serving a website from, like auth. or api. Do not use your apex/root domain. |
| Pro plan or above | Custom 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.
- Enter
auth.yourgame.comand click Connect domain. The panel shows a status of Waiting on DNS. - Add the DNS records the panel shows you at your registrar (details below). There is one
CNAMEthat does the routing, and usually oneTXTthat proves you own the domain. - 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.
- 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:
| Type | Name | Points to | Why |
|---|---|---|---|
| CNAME | auth.yourgame.com | ssl.evora.lol | Routes 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. |
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.
- 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.
// 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.
#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.
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:
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
https://api.evora.lol/api/developer-api
Quick start
curl -H "Authorization: Bearer ag_sk_YOUR_KEY" \ https://api.evora.lol/api/developer-api/users
# 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}'
# 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 × 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.
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:
# 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.
// 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.
// 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 }
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.
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.
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.
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})`);
await fetch(`${API}/password-reset/fulfil`, { method: 'POST', headers: H, body: JSON.stringify({ token, newPassword }), }); // -> { success: true, userId, username, sessions_terminated }
| Property | Behaviour |
|---|---|
| Storage | Only a SHA-256 hash is stored — a database leak is not replayable |
| Single use | Fulfilling marks it used; a replay returns INVALID_TOKEN |
| Siblings | Fulfilling (or any password change) voids every other outstanding token for that user |
| Sessions | A successful reset kills the user's live SDK sessions |
| Tenancy | A token can only be fulfilled by the developer who owns the user |
| Limits | Max 3 live tokens per user; 5 issues/min per user; expired/used/unknown all return the same error |
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.
| Method | Path | Description |
|---|---|---|
| POST | /users/:userId/subscriptions/:appId/pause | Freeze — banks remaining seconds |
| POST | /users/:userId/subscriptions/:appId/unpause | Resume — restores banked time |
| POST | /apps/:appId/licenses/:licenseId/pause | Freeze a single license key |
| POST | /apps/:appId/licenses/:licenseId/unpause | Resume 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 feature | Endpoint |
|---|---|
| Redeem a key | POST /users/:userId/redeem |
| Show subscriptions (incl. lapsed) | GET /users/:userId/subscriptions?includeExpired=true |
| Reset HWID (cooldown enforced) | POST /users/:userId/reset-hwid |
| Reset device binding | POST /users/:userId/reset-device |
| Change password | PUT /users/:userId |
Applications
Scopes: apps:read / apps:write
| Method | Path | Description |
|---|---|---|
| GET | /apps | List your applications |
| GET | /apps/:appId | Get application details |
| POST | /apps | Create application |
| PUT | /apps/:appId | Update application |
| DELETE | /apps/:appId | Delete application |
| GET | /apps/:appId/stats | Get app statistics |
Two fields on PUT /apps/:appId govern the end-user account surface:
| Field | Values | Meaning |
|---|---|---|
| twofa_policy | disabled | optional | required | Whether your users may (or must) enrol a second factor. Defaults to optional. |
| stats_public | true | false | Publishes user/licence/online counts to the SDK via FetchStats. Defaults to false. |
Statistics
Scope: stats:read
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/stats/overview | Extended overview (users, licenses, sessions, auth trends) |
| GET | /apps/:appId/stats/logins | Login activity by day (?days=30) |
| GET | /apps/:appId/stats/users | User growth over time (?days=30) |
| GET | /apps/:appId/stats/licenses | License status breakdown |
| GET | /apps/:appId/stats/sessions | Session trends (?days=7) |
Users
Scopes: users:read / users:write
| Method | Path | Description |
|---|---|---|
| POST | /apps/:appId/users/authenticate | Verify user credentials (panel login) |
| GET | /apps/:appId/users/lookup?username= | Look up user by username |
| GET | /users | List users (paginated, ?appId= filter) |
| GET | /users/:userId | Get single user |
| POST | /users | Create user (username + password required) |
| PUT | /users/:userId | Update user (username, email, password) |
| DELETE | /users/:userId | Delete user |
| POST | /users/:userId/ban | Ban user (optional HWID/IP blacklist cascade) |
| POST | /users/:userId/unban | Unban user |
| POST | /users/:userId/reset-hwid | Reset HWID (cooldown enforced) |
| POST | /users/:userId/reset-device | Reset device binding |
| GET | /users/:userId/2fa | Two-factor state (enabled, backup codes left, lockout) |
| DELETE | /users/:userId/2fa | Support reset — clears 2FA and ends live sessions |
| POST | /users/:userId/redeem | Redeem a license key for this user (licenses:write) |
| POST | /users/:userId/password-reset | Mint a single-use reset token (you deliver it) |
| POST | /password-reset/fulfil | Consume a reset token and set the new password |
| POST | /users/:userId/subscriptions/:appId/pause | Freeze a subscription (banks remaining time) |
| POST | /users/:userId/subscriptions/:appId/unpause | Resume a frozen subscription |
| POST | /users/:userId/issue-session-token | Mint a one-time SDK login token (SSO) |
| GET | /users/:userId/subscriptions | List subscriptions (?includeExpired=true) |
| POST | /users/:userId/subscriptions | Add subscription |
| POST | /users/:userId/subscriptions/extend | Extend subscription |
| DELETE | /users/:userId/subscriptions/:appId | Remove subscription |
| POST | /users/bulk | Bulk ban/unban/delete/reset-hwid/extend |
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
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/licenses | List licenses (paginated, searchable) |
| GET | /apps/:appId/licenses/:licenseId | Get single license |
| POST | /apps/:appId/licenses | Generate licenses |
| PUT | /apps/:appId/licenses/:licenseId | Update license |
| DELETE | /apps/:appId/licenses/:licenseId | Delete license |
| POST | /apps/:appId/licenses/:id/ban | Ban license |
| POST | /apps/:appId/licenses/:id/unban | Unban license |
| POST | /apps/:appId/licenses/:id/reset-hwid | Reset license HWID |
| POST | /apps/:appId/licenses/:keyOrId/expiry | Extend or reduce expiry by a signed delta (takes the raw key) |
| POST | /apps/:appId/licenses/bulk | Bulk actions — see below |
| POST | /apps/:appId/licenses/authenticate | Verify 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.
# 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" }
| Action | Requires |
|---|---|
| delete_selected / ban_selected / unban_selected | ids[] (reason optional for ban) |
| pause_selected / unpause_selected / reset_hwid_selected | ids[] |
| extend_selected | ids[], durationSeconds |
| delete_unused / delete_all | — |
| add_time | durationSeconds (applies to unused keys) |
| ban_all / unban_all / pause_all / unpause_all / reset_hwid_all / delete_all_matching | sourceFilter: all | developer | reseller |
| extend_all | durationSeconds, sourceFilter |
Variables
Scopes: variables:read / variables:write
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/variables | List app variables |
| GET | /apps/:appId/variables/:key | Get one variable by key |
| POST | /apps/:appId/variables | Create or update a variable (upsert) |
| PUT | /apps/:appId/variables/:key | Update variable by key |
| DELETE | /apps/:appId/variables/:key | Delete variable by key |
| DELETE | /apps/:appId/variables | Delete all app variables |
| GET | /apps/:appId/user-variables | List every user variable in the app |
| GET | /apps/:appId/users/:userId/variables | List one user's variables |
| POST | /apps/:appId/users/:userId/variables | Set a user variable |
| DELETE | /apps/:appId/users/:userId/variables/:varKey | Delete a user variable |
| DELETE | /apps/:appId/users/:userId/variables | Delete all of a user's variables |
var_key, not by an id. PUT is an upsert, so writing to an unknown key creates it.Webhooks
Scopes: webhooks:read / webhooks:write
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/webhooks | List webhooks |
| POST | /apps/:appId/webhooks | Create webhook |
| PUT | /apps/:appId/webhooks/:webhookId | Update webhook |
| DELETE | /apps/:appId/webhooks/:webhookId | Delete webhook |
| POST | /apps/:appId/webhooks/:webhookId/test | Test webhook |
| GET | /apps/:appId/events | Read 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.
{ "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" }
GET /apps/:appId/seller-sharing-alerts and resolve it with POST /apps/:appId/seller-sharing-alerts/:alertId/review (reviewed, confirmed or dismissed).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.
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.
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.
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
| Event | Fires when |
|---|---|
| user.register | An end-user registers through the SDK |
| user.login | An end-user authenticates |
| user.banned / user.unbanned | You ban or reinstate a customer |
| license.used | A key is redeemed — from any surface; check `source` |
| subscription.created | A customer gains a subscription (key or API grant) |
| subscription.extended | Time is added to an existing subscription |
| subscription.paused / .resumed | A subscription is frozen or resumed |
| subscription.removed | A subscription is revoked |
| hwid.reset | A customer's hardware binding is cleared |
| blacklist.blocked | A blacklisted HWID/IP/username is refused |
| anti_debug / anti_vm / anti_hv / anti_http_debug / anti_attach .detected | SDK protection triggers |
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
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/blacklist | List blacklist entries |
| POST | /apps/:appId/blacklist | Add blacklist entry |
| DELETE | /apps/:appId/blacklist/:entryId | Remove blacklist entry |
| GET | /apps/:appId/whitelist | List whitelist entries |
| POST | /apps/:appId/whitelist | Add whitelist entry |
| DELETE | /apps/:appId/whitelist/:entryId | Remove whitelist entry |
Sellers (API)
Scopes: sellers:read / sellers:write
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/sellers | List sellers |
| GET | /apps/:appId/sellers/:sellerId | Get seller |
| POST | /apps/:appId/sellers | Create seller |
| PUT | /apps/:appId/sellers/:sellerId | Update seller |
| POST | /apps/:appId/sellers/:sellerId/balance | Add balance |
| DELETE | /apps/:appId/sellers/:sellerId | Delete seller |
Logs & Sessions (API)
Scopes: logs:read / apps:read (sessions)
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/logs | List logs (filter by type, user, date) |
| GET | /apps/:appId/logs/stats | Log statistics |
| GET | /apps/:appId/sessions | List active sessions |
| DELETE | /apps/:appId/sessions/:sessionId | Kill session |
Subscription Tiers (API)
Scopes: apps:read / apps:write
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/subscriptions | List subscription tiers |
| POST | /apps/:appId/subscriptions | Create subscription tier |
| PUT | /apps/:appId/subscriptions/:id | Update subscription tier |
| DELETE | /apps/:appId/subscriptions/:id | Delete subscription tier |
Entitlements, Geo & Floating
Scopes: entitlements:read/write · geo:read/write · floating:read/write
| Method | Path | Description |
|---|---|---|
| GET | /apps/:appId/entitlements | List entitlements |
| POST | /apps/:appId/entitlements | Create entitlement |
| PUT | /apps/:appId/entitlements/:entitlementId | Update entitlement |
| DELETE | /apps/:appId/entitlements/:entitlementId | Delete entitlement |
| GET | /apps/:appId/subscriptions/:subscriptionId/entitlements | Entitlements on a tier |
| POST | /apps/:appId/subscriptions/:subscriptionId/entitlements | Attach entitlement to a tier |
| GET | /apps/:appId/users/:userId/entitlements | Resolve a user's entitlements |
| GET | /apps/:appId/geo-rules | List geo rules |
| POST | /apps/:appId/geo-rules | Add geo rule |
| DELETE | /apps/:appId/geo-rules/:ruleId | Remove geo rule |
| PUT | /apps/:appId/geo-enabled | Enable/disable geo restrictions |
| GET | /apps/:appId/floating/leases | List floating leases |
| DELETE | /apps/:appId/floating/leases/:leaseId | Revoke a lease |
| GET | /apps/:appId/users/:userId/floating/seats | Seat 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.
| Method | Path | Description |
|---|---|---|
| GET | /clients | List clients |
| POST | /clients | Create client |
| PUT | /clients/:clientId | Update client |
| DELETE | /clients/:clientId | Delete client |
| GET | /clients/:clientId/apps | List a client's app access |
| POST | /clients/:clientId/apps | Grant app access |
| DELETE | /clients/:clientId/apps/:appId | Revoke app access |
Quota
Scope: apps:read
| Method | Path | Description |
|---|---|---|
| GET | /quota | Plan 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
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.
curl -H "Authorization: Bearer ag_rk_YOUR_KEY" \ https://api.evora.lol/api/reseller-api/me
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.
| Scope | Grants |
|---|---|
| licenses:generate | Mint new keys. This is what a shop integration needs. |
| licenses:read | List and look up keys you own. |
| licenses:manage | Reset HWID, ban, unban, delete. Only for something that handles support. |
| balance:read | Read remaining credit per duration. |
| webhooks:read | List webhook endpoints. |
| webhooks:write | Create, edit, delete and test webhook endpoints. |
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.
| Situation | Response |
|---|---|
| First call with this key | Mints normally, and the response is stored. |
| Retry, same key, same body | Replays the original response with an Idempotent-Replay: true header. Nothing is minted, nothing is charged. |
| Retry while the first is still running | 409 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 body | 409 idempotency_key_reused. That is a client bug, so it is surfaced rather than hidden behind a replay. |
| Retry after a failure | Failures are not cached, so a call that hit insufficient_balance can be retried against the same key once you top up. |
Minting keys
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}'
{ "success": true, "count": 1, "keys": ["A1B2C-D3E4F-G5H6I-J7K8L-M9N0P"], "cost": { "unit": "month", "credits": 1 } }
Request body
| Field | Default | Meaning |
|---|---|---|
| amount | 1 | How many keys to mint, 1 to 100. |
| expiry | day | Which credit bucket to spend, and the unit duration is counted in: hour, day, week, month, 3month, 6month, year, lifetime. |
| duration | 1 | How many of that unit each key lasts. |
| level | 1 | Subscription level. Must be one your developer allows — see allowed_levels on GET /me. |
| mask | *****-*****-*****-*****-***** | Key format. Each * becomes a random character. |
| note | null | Free text stored against the keys, up to 255 chars. Handy for your order id. |
| uppercase / lowercase | true / false | Character case of generated keys. |
What a mint costs
Credits are metered by the access a mint creates, not by the number of keys:
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.
| Request | Cost |
|---|---|
| amount 1, duration 1, expiry month | 1 month credit |
| amount 10, duration 1, expiry month | 10 month credits |
| amount 1, duration 30, expiry day | 30 day credits |
| amount 5, duration 24, expiry hour | 120 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.
{ "quote": { "unit": "day", "amount": 1, "duration": 30, "cost": 30, "available": 120, "affordable": true, "durationSeconds": 2592000 } }
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
# 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.
{ "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.
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 & path | Scope |
|---|---|
| GET /me | none — any valid key |
| GET /balance | balance:read |
| POST /licenses | licenses:generate |
| POST /licenses/quote | balance:read |
| GET /licenses | licenses:read |
| GET /licenses/:id | licenses:read |
| POST /licenses/:id/hwid-reset | licenses:manage |
| POST /licenses/:id/ban | licenses:manage |
| POST /licenses/:id/unban | licenses:manage |
| DELETE /licenses/:id | licenses:manage |
| GET /webhooks | webhooks:read |
| POST /webhooks | webhooks:write |
| PUT /webhooks/:id | webhooks:write |
| DELETE /webhooks/:id | webhooks:write |
| POST /webhooks/:id/test | webhooks:write |
| POST /webhooks/:id/rotate-secret | webhooks:write |
| GET /webhooks/:id/deliveries | webhooks:read |
Error codes
Every error carries a stable code alongside the human-readable error. Branch on the code, never on the message.
| Code | Status | Meaning |
|---|---|---|
| invalid_api_key | 401 | Unknown, revoked or expired key. |
| unauthenticated | 401 | No Authorization header and no panel session. |
| ip_not_allowed | 403 | Key is pinned to a different address. |
| insufficient_scope | 403 | Key lacks the scope this route needs. |
| seller_disabled | 403 | Your reseller account has been disabled by the developer. |
| cannot_create_licenses | 403 | Key minting is turned off for your account. |
| level_not_allowed | 403 | Requested level is outside the levels you may sell. |
| license_limit_reached | 403 | You have hit the total key cap set by the developer. |
| app_mismatch | 403 | Key was issued for a different application. |
| insufficient_balance | 402 | Not enough credit in that bucket. Body carries required and available. Top up and retry with the same Idempotency-Key. |
| duration_too_long | 400 | duration exceeds the cap for that unit. |
| idempotency_key_required | 400 | POST /licenses was called without an Idempotency-Key header. |
| idempotency_in_progress | 409 | An identical request is still running. |
| idempotency_key_reused | 409 | Key reused with a different body. |
| license_not_found | 404 | No such key, or it is not one of yours. |
| rate_limited | 429 | Over 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
| Event | Fires when |
|---|---|
| license.redeemed | A key you sold was activated by an end user, on any surface: loader, account panel or API. |
| balance.low | A sale took one of your duration buckets down to your threshold. |
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
{ "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" }
{ "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.
| Header | Contains |
|---|---|
| X-Evora-Signature | HMAC-SHA256 hex digest. |
| X-Evora-Timestamp | Unix seconds, the value signed alongside the body. |
| X-Evora-Event-Id | Stable id for this event. Retries reuse it, so record it to dedupe. |
| X-Evora-Delivery-Attempt | 1 on the first try, incrementing per retry. |
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.
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.
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
| Scope | Access |
|---|---|
| apps:read | List and view applications, stats, blacklist, whitelist, sessions |
| apps:write | Create, update, delete apps. Manage blacklist, whitelist, sessions. |
| licenses:read | List and view licenses for an app |
| licenses:write | Create, update, delete, ban/unban licenses. Bulk operations. |
| users:read | List and view users and their subscriptions |
| users:write | Create, update, delete, ban/unban users. HWID/device reset. Bulk operations. |
| variables:read | Read app variables and per-user variables |
| variables:write | Create, update, delete app and per-user variables |
| webhooks:read | List and view webhooks |
| webhooks:write | Create, update, delete, test webhooks |
| sellers:read | List and view seller accounts |
| sellers:write | Create, update, delete sellers. Manage balances. |
| logs:read | View application logs and log statistics |
| stats:read | Dashboard analytics: overview, login trends, user growth, aggregates |
| entitlements:read | View entitlements and their subscription/user bindings |
| entitlements:write | Create, update, delete, and attach entitlements |
| geo:read | View geo restriction rules |
| geo:write | Add, remove, and toggle geo restriction rules |
| floating:read | View floating license leases and seat usage |
| floating:write | Revoke 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
Authorization: Bearer ag_sk_your_api_key_here
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:
| Field | Description |
|---|---|
| name | Display name (e.g. "Basic", "Premium", "Lifetime") |
| level | Numeric level. Higher means more access. Use this in your app to gate features. |
| duration | How long the subscription lasts (days, or lifetime) |
Using tiers in your app
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
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
# 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
# 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:
| Method | Returns | Description |
|---|---|---|
| Initialized() | bool | Whether Init() completed successfully |
| Authenticated() | bool | Whether Login/Register/License succeeded |
| LastError() | std::string | Human-readable last error message |
| LastErrorCode() | ErrorCode | Enum value of last error |
| GetSdkVersion() | std::string | SDK version string (e.g. "2.9.7") |
| GetAuthMode() | std::string | App's configured auth mode ("license", "user_pass", "both") |
| GetAppName() | std::string | Application name from the dashboard |
| IsBlacklisted() | bool | Whether current HWID is blacklisted |
| IsWebSocketConnected() | bool | Whether the WebSocket connection is active |
| User() | const UserData& | Authenticated user's data |
| Wait() | void | Blocks until Close() is called or the process exits |
| Close() | void | Shuts down all background threads and disconnects |
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:
| Define | Effect |
|---|---|
| EVORION_SDK_VERSION | Defined automatically by the header. Use for compile-time version checks. |
| EVORION_NO_AUTOLINK | Disables the #pragma comment(lib, ...) directives. Define this if you link system libraries manually or use a custom build system. |
| EVORION_NO_ANTIDEBUG | Strips all anti-debug code at compile time. Useful for debug builds where you need to attach a debugger. |
| EVORION_NO_PROTECT | Strips all code protection macros (PROTECT, PROTECT_SEH, SPLIT, FLOW) so they compile to nothing. |
| EVORION_NO_TLS_CALLBACKS | Disables TLS callback registration. Define this if you handle TLS callbacks yourself or use a packer that conflicts with them. |
#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:
#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
| Macro | Purpose |
|---|---|
| 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 / END | Byte-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
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.
All 109 endpoints, grouped. Each has its own page with parameters, response body and required scope — browse the full reference.
Quota
Applications
Statistics
Licenses
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
Subscriptions
Subscription tiers
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