API Docs
Everything you need to integrate vshll licensing and push live updates to your users.
How auto-updates work
- You ship
v1.0.0to your users as an.exe. - A month later you upload
v1.2.0from the dashboard and mark it active. - When the user opens their old exe, it calls
/api/public/v1/update. - The server compares versions and returns a signed 5-minute download URL.
- Your updater downloads it, verifies the SHA-256, replaces the binary, and relaunches.
Tracking the installed version (stop re-downloading)
If your app redownloads on every launch, it's because current_version never changes — a hardcoded constant in the new exe is still whatever you compiled it as. The server only skips the download when the version you send matches the latest active version.
Fix: store the installed version in a tiny version.txt next to the exe. Read it at startup, write the new value right after a successful update. Bonus: you no longer need to rebuild with a different constant for every release.
Python — version helper
# version.py — read/write the installed version next to the exe
import os, sys
def _version_file():
base = os.path.dirname(sys.executable if getattr(sys, "frozen", False) else __file__)
return os.path.join(base, "version.txt")
FALLBACK_VERSION = "1.0.0" # only used the very first run
def get_current_version():
try:
with open(_version_file(), "r") as f:
return f.read().strip() or FALLBACK_VERSION
except FileNotFoundError:
return FALLBACK_VERSION
def set_current_version(v):
with open(_version_file(), "w") as f:
f.write(v.strip())
Use it in your updater:
from version import get_current_version, set_current_version
CURRENT_VERSION = get_current_version()
# inside your updater worker, AFTER the .bat is written and BEFORE relaunch:
set_current_version(info["latest_version"])
C++ / Qt — version helper
// version.hpp
#pragma once
#include <QCoreApplication>
#include <QFile>
#include <QFileInfo>
#include <QString>
inline QString versionFilePath() {
return QFileInfo(QCoreApplication::applicationFilePath()).absolutePath() + "/version.txt";
}
inline QString getCurrentVersion(const QString& fallback = "1.0.0") {
QFile f(versionFilePath());
if (!f.open(QIODevice::ReadOnly)) return fallback;
QString v = QString::fromUtf8(f.readAll()).trimmed();
return v.isEmpty() ? fallback : v;
}
inline void setCurrentVersion(const QString& v) {
QFile f(versionFilePath());
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate)) f.write(v.trimmed().toUtf8());
}
Then replace the hardcoded CURRENT_VERSION with getCurrentVersion(), and call setCurrentVersion(latest) after the download verifies but before the relaunch.
C# / .NET — version helper
// VersionFile.cs — read/write the installed version next to the exe
using System;
using System.IO;
public static class VersionFile {
public const string Fallback = "1.0.0";
private static string Path =>
System.IO.Path.Combine(AppContext.BaseDirectory, "version.txt");
public static string Get() {
try { var v = File.ReadAllText(Path).Trim();
return string.IsNullOrEmpty(v) ? Fallback : v; }
catch (FileNotFoundException) { return Fallback; }
}
public static void Set(string v) => File.WriteAllText(Path, v.Trim());
}
Call VersionFile.Get() when building your update-check request. You don't need to call VersionFile.Set() yourself — the updater's swap script writes version.txt atomically after the new binary is in place.
version.txt only after the SHA-256 check passes and the new binary is in place — otherwise a failed update will convince your app it's already on the new version and you'll be stuck.POST /api/public/v1/update
Request body:
{
"api_key": "app_xxx",
"license_key": "XXXX-XXXX-XXXX-XXXX-XXXX",
"hwid": "device-fingerprint",
"current_version": "1.0.0"
}Response:
{
"update_available": true,
"latest_version": "1.2.0",
"current_version": "1.0.0",
"mandatory": false,
"notes": "Bug fixes and new UI",
"file_size": 8421376,
"sha256": "a1b2c3...",
"download_url": "https://...signed-url...?exp=..."
}Python updater (Tkinter + progress bar)
Drop this into your app and call maybe_update(license_key, hwid) at startup.
# pip install requests
# Assumes you also have version.py from the section above
import os, sys, hashlib, tempfile, subprocess, threading, requests
import tkinter as tk
from tkinter import ttk
from version import get_current_version, set_current_version
API = "https://vshll.lol/api/public/v1"
API_KEY = "YOUR_APP_API_KEY"
def check_for_update(license_key, hwid):
r = requests.post(f"{API}/update", json={
"api_key": API_KEY,
"license_key": license_key,
"hwid": hwid,
"current_version": get_current_version(),
}, timeout=15)
r.raise_for_status()
return r.json()
def download_with_progress(url, dest, sha256, on_progress):
h = hashlib.sha256()
with requests.get(url, stream=True, timeout=60) as r:
r.raise_for_status()
total = int(r.headers.get("content-length", 0))
done = 0
with open(dest, "wb") as f:
for chunk in r.iter_content(1024 * 64):
f.write(chunk); h.update(chunk); done += len(chunk)
if total: on_progress(done / total)
if sha256 and h.hexdigest() != sha256:
raise RuntimeError("Checksum mismatch — file tampered")
def show_updater(latest, url, sha256):
root = tk.Tk()
root.title("Updating vshll App")
root.geometry("380x140"); root.configure(bg="#0a0a0a")
tk.Label(root, text=f"Updating to v{latest}…", fg="white", bg="#0a0a0a",
font=("Segoe UI", 11, "bold")).pack(pady=(20, 8))
status = tk.Label(root, text="Downloading…", fg="#888", bg="#0a0a0a")
status.pack()
bar = ttk.Progressbar(root, length=320, mode="determinate", maximum=100)
bar.pack(pady=14)
tmp = os.path.join(tempfile.gettempdir(), "vshll_update.exe")
def worker():
try:
download_with_progress(url, tmp, sha256,
lambda p: (bar.config(value=p*100), root.update_idletasks()))
status.config(text="Installing…")
# Atomic swap with backup + rollback. version.txt is written by the
# .bat ONLY after the move succeeds — never trust a half-applied update.
current = sys.executable
backup = current + ".bak"
vfile = os.path.join(os.path.dirname(current), "version.txt")
pid = os.getpid()
bat = current + ".update.bat"
with open(bat, "w") as f:
f.write(
f'@echo off\n'
f'setlocal\n'
f':wait\n'
f'tasklist /FI "PID eq {pid}" 2>NUL | find "{pid}" >NUL\n'
f'if not errorlevel 1 (timeout /t 1 /nobreak >NUL & goto wait)\n'
f'if exist "{backup}" del /F /Q "{backup}"\n'
f'move /Y "{current}" "{backup}" || (timeout /t 2 /nobreak >NUL & move /Y "{current}" "{backup}")\n'
f'move /Y "{tmp}" "{current}"\n'
f'if errorlevel 1 (echo rollback & move /Y "{backup}" "{current}" & start "" "{current}" & del "%%~f0" & exit /b 1)\n'
f'> "{vfile}" echo {latest}\n'
f'del /F /Q "{backup}" 2>NUL\n'
f'start "" "{current}"\n'
f'del "%%~f0"\n'
)
subprocess.Popen(["cmd", "/c", bat],
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0))
root.destroy(); sys.exit(0)
except Exception as e:
status.config(text=f"Update failed: {e}")
threading.Thread(target=worker, daemon=True).start()
root.mainloop()
# Call this at app startup, BEFORE your main UI
def maybe_update(license_key, hwid):
info = check_for_update(license_key, hwid)
if info.get("update_available"):
show_updater(info["latest_version"], info["download_url"], info.get("sha256"))
C++ updater (Qt6 + progress bar)
Requires Qt 6 (Widgets + Network). Show Updater before your main window.
// C++17 + Qt 6 (Widgets + Network). Link: Qt6::Widgets Qt6::Network
#include <QApplication>
#include <QProgressBar>
#include <QLabel>
#include <QVBoxLayout>
#include <QWidget>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QJsonDocument>
#include <QJsonObject>
#include <QCryptographicHash>
#include <QFile>
#include <QProcess>
#include <QStandardPaths>
#include "version.hpp"
static const QString API_BASE = "https://vshll.lol/api/public/v1";
static const QString API_KEY = "YOUR_APP_API_KEY";
class Updater : public QWidget {
Q_OBJECT
public:
Updater(QString licenseKey, QString hwid) {
setWindowTitle("Updating vshll App");
resize(380, 140);
auto* lay = new QVBoxLayout(this);
statusLbl = new QLabel("Checking for updates…");
bar = new QProgressBar(); bar->setRange(0, 100);
lay->addWidget(statusLbl); lay->addWidget(bar);
checkUpdate(licenseKey, hwid);
}
private:
QNetworkAccessManager nm;
QLabel* statusLbl; QProgressBar* bar;
QByteArray expectedSha;
QString latestVersion;
void checkUpdate(const QString& key, const QString& hwid) {
QJsonObject body{{"api_key", API_KEY}, {"license_key", key},
{"hwid", hwid}, {"current_version", getCurrentVersion()}};
QNetworkRequest req(QUrl(API_BASE + "/update"));
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
auto* reply = nm.post(req, QJsonDocument(body).toJson());
connect(reply, &QNetworkReply::finished, [this, reply]{
auto json = QJsonDocument::fromJson(reply->readAll()).object();
reply->deleteLater();
if (!json.value("update_available").toBool()) { close(); return; }
expectedSha = json.value("sha256").toString().toUtf8();
latestVersion = json.value("latest_version").toString();
statusLbl->setText("Downloading v" + latestVersion);
download(json.value("download_url").toString());
});
}
void download(const QString& url) {
auto* reply = nm.get(QNetworkRequest(QUrl(url)));
auto* tmp = new QFile(QStandardPaths::writableLocation(
QStandardPaths::TempLocation) + "/vshll_update.exe", this);
tmp->open(QIODevice::WriteOnly);
QCryptographicHash* h = new QCryptographicHash(QCryptographicHash::Sha256);
connect(reply, &QNetworkReply::downloadProgress, [this](qint64 a, qint64 b){
if (b > 0) bar->setValue(int(100 * a / b));
});
connect(reply, &QNetworkReply::readyRead, [reply, tmp, h]{
auto data = reply->readAll(); tmp->write(data); h->addData(data);
});
connect(reply, &QNetworkReply::finished, [this, reply, tmp, h]{
tmp->close(); reply->deleteLater();
if (!expectedSha.isEmpty() && h->result().toHex() != expectedSha) {
statusLbl->setText("Checksum failed — aborting."); delete h; return;
}
delete h;
statusLbl->setText("Installing…");
installAndRelaunch(tmp->fileName());
});
}
void installAndRelaunch(const QString& newExe) {
QString cur = QCoreApplication::applicationFilePath();
QString bat = cur + ".update.bat";
qint64 pid = QCoreApplication::applicationPid();
QString backup = cur + ".bak";
QString vfile = QFileInfo(cur).absolutePath() + "/version.txt";
QFile f(bat); f.open(QIODevice::WriteOnly);
// Backup + atomic swap + rollback. version.txt is written ONLY on success.
f.write(QString(
"@echo off\n"
"setlocal\n"
":wait\n"
"tasklist /FI \"PID eq %5\" 2>NUL | find \"%5\" >NUL\n"
"if not errorlevel 1 (timeout /t 1 /nobreak >NUL & goto wait)\n"
"if exist \"%3\" del /F /Q \"%3\"\n"
"move /Y \"%2\" \"%3\" || (timeout /t 2 /nobreak >NUL & move /Y \"%2\" \"%3\")\n"
"move /Y \"%1\" \"%2\"\n"
"if errorlevel 1 (move /Y \"%3\" \"%2\" & start \"\" \"%2\" & del \"%%~f0\" & exit /b 1)\n"
"> \"%4\" echo %6\n"
"del /F /Q \"%3\" 2>NUL\n"
"start \"\" \"%2\"\n"
"del \"%%~f0\"\n"
).arg(newExe, cur, backup, vfile, QString::number(pid), latestVersion).toUtf8());
f.close();
QProcess::startDetached("cmd", {"/c", bat});
QCoreApplication::quit();
}
};
// In main(): Updater u(licenseKey, hwid); u.show(); // BEFORE launching main window
C# updater (.NET 6+ WPF + progress bar)
Built-in HttpClient + WPF — no NuGet dependencies. Call await Updater.RunAsync(licenseKey, hwid) in App.OnStartup before showing your main window.
// .NET 6+ WPF (or WinForms). NuGet: none — uses built-in HttpClient + WPF.
// Show UpdaterWindow at startup BEFORE your MainWindow.
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Windows;
public record UpdateResp(
[property: JsonPropertyName("update_available")] bool UpdateAvailable,
[property: JsonPropertyName("latest_version")] string? LatestVersion,
[property: JsonPropertyName("download_url")] string? DownloadUrl,
[property: JsonPropertyName("sha256")] string? Sha256,
[property: JsonPropertyName("mandatory")] bool Mandatory);
public class Updater {
const string API = "https://vshll.lol/api/public/v1";
const string API_KEY = "YOUR_APP_API_KEY";
static readonly HttpClient http = new() { Timeout = TimeSpan.FromMinutes(5) };
public static async Task RunAsync(string licenseKey, string hwid) {
var body = new {
api_key = API_KEY, license_key = licenseKey, hwid,
current_version = VersionFile.Get()
};
var resp = await http.PostAsJsonAsync($"{API}/update", body);
resp.EnsureSuccessStatusCode();
var info = await resp.Content.ReadFromJsonAsync<UpdateResp>();
if (info is null || !info.UpdateAvailable) return;
var win = new UpdaterWindow(info.LatestVersion!);
win.Show();
try {
var tmp = Path.Combine(Path.GetTempPath(), "vshll_update.exe");
await DownloadAsync(info.DownloadUrl!, tmp, info.Sha256, win.SetProgress);
win.SetStatus("Installing…");
InstallAndRelaunch(tmp, info.LatestVersion!);
Application.Current.Shutdown();
} catch (Exception e) {
win.SetStatus("Update failed: " + e.Message);
if (info.Mandatory) Application.Current.Shutdown();
}
}
static async Task DownloadAsync(string url, string dest, string? sha, Action<double> onProgress) {
using var r = await http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
r.EnsureSuccessStatusCode();
var total = r.Content.Headers.ContentLength ?? 0L;
await using var src = await r.Content.ReadAsStreamAsync();
await using var dst = File.Create(dest);
using var sha256 = SHA256.Create();
var buf = new byte[64 * 1024]; long done = 0; int n;
while ((n = await src.ReadAsync(buf)) > 0) {
await dst.WriteAsync(buf.AsMemory(0, n));
sha256.TransformBlock(buf, 0, n, null, 0); done += n;
if (total > 0) onProgress((double)done / total);
}
sha256.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
var got = Convert.ToHexString(sha256.Hash!).ToLowerInvariant();
if (!string.IsNullOrEmpty(sha) && !string.Equals(got, sha, StringComparison.OrdinalIgnoreCase))
throw new InvalidDataException("Checksum mismatch — file tampered");
}
static void InstallAndRelaunch(string newExe, string latest) {
var cur = Process.GetCurrentProcess().MainModule!.FileName!;
var backup = cur + ".bak";
var vfile = Path.Combine(Path.GetDirectoryName(cur)!, "version.txt");
var pid = Environment.ProcessId;
var bat = cur + ".update.bat";
// Backup + atomic swap + rollback. version.txt written ONLY on success.
File.WriteAllText(bat, $@"@echo off
setlocal
:wait
tasklist /FI ""PID eq {pid}"" 2>NUL | find ""{pid}"" >NUL
if not errorlevel 1 (timeout /t 1 /nobreak >NUL & goto wait)
if exist ""{backup}"" del /F /Q ""{backup}""
move /Y ""{cur}"" ""{backup}"" || (timeout /t 2 /nobreak >NUL & move /Y ""{cur}"" ""{backup}"")
move /Y ""{newExe}"" ""{cur}""
if errorlevel 1 (move /Y ""{backup}"" ""{cur}"" & start """" ""{cur}"" & del ""%~f0"" & exit /b 1)
> ""{vfile}"" echo {latest}
del /F /Q ""{backup}"" 2>NUL
start """" ""{cur}""
del ""%~f0""
");
Process.Start(new ProcessStartInfo("cmd", $"/c \"{bat}\"") {
CreateNoWindow = true, UseShellExecute = false
});
}
}
// UpdaterWindow.xaml.cs — tiny WPF window with a progress bar
public partial class UpdaterWindow : Window {
public UpdaterWindow(string version) {
InitializeComponent();
Title = $"Updating to v{version}";
}
public void SetProgress(double p) =>
Dispatcher.Invoke(() => Bar.Value = p * 100);
public void SetStatus(string s) =>
Dispatcher.Invoke(() => StatusLabel.Content = s);
}
/* UpdaterWindow.xaml:
<Window x:Class="YourApp.UpdaterWindow" Width="380" Height="140"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize">
<StackPanel Margin="20">
<Label x:Name="StatusLabel" Content="Downloading…"/>
<ProgressBar x:Name="Bar" Height="18" Minimum="0" Maximum="100"/>
</StackPanel>
</Window>
*/
Works the same in WinForms — replace Window with a small Form hosting a ProgressBar. For Avalonia, swap Dispatcher.Invoke for Dispatcher.UIThread.Post.
Admin API — build your own dashboard / Discord bot
Every endpoint below authenticates with your app's secret_key (find it in the dashboard → app settings). Treat it like a password — never ship it inside the app you give to end users. Use it from your own backend, a private script, a Discord bot, a CRM webhook, anything you control.
https://vshll.lol/api/public/v1/adminAuthorization: Bearer YOUR_APP_SECRET_KEYEndpoints
| Method | Path | What it does |
|---|---|---|
| GET | /licenses | List up to 500 keys (newest first) |
| POST | /licenses | Generate 1–100 keys. Body: count, expires_in_days, max_activations, note, ip_lock |
| GET | /licenses/:key | Fetch one key + its status |
| DELETE | /licenses/:key | Revoke (permanently delete) a key |
| POST | /licenses/:key/ban | Blacklist — validation immediately fails |
| POST | /licenses/:key/unban | Re-enable a banned key |
| POST | /licenses/:key/reset | Clear all HWID activations (user can re-activate on a new machine) |
| GET | /logs | Last 200 audit events for the app |
Python client (use anywhere — scripts, Flask dashboards, automations)
# pip install requests
# Build your own dashboard, Discord bot, CRM hook — anything that should manage licenses.
# Use your APP SECRET KEY (from the dashboard), NEVER ship this in your end-user app.
import requests
API = "https://vshll.lol/api/public/v1/admin"
SECRET = "YOUR_APP_SECRET_KEY"
H = {"Authorization": f"Bearer {SECRET}", "Content-Type": "application/json"}
class Vshll:
# ---- list / fetch ----
def list_licenses(self):
return requests.get(f"{API}/licenses", headers=H, timeout=15).json()
def get_license(self, key):
return requests.get(f"{API}/licenses/{key}", headers=H, timeout=15).json()
# ---- create ----
def create(self, count=1, expires_in_days=30, max_activations=1, note=None):
body = {"count": count, "expires_in_days": expires_in_days,
"max_activations": max_activations, "note": note}
return requests.post(f"{API}/licenses", headers=H, json=body, timeout=15).json()
# ---- moderation ----
def ban(self, key): return requests.post(f"{API}/licenses/{key}/ban", headers=H, timeout=15).json()
def unban(self, key): return requests.post(f"{API}/licenses/{key}/unban", headers=H, timeout=15).json()
def reset(self, key): return requests.post(f"{API}/licenses/{key}/reset", headers=H, timeout=15).json() # clear HWIDs
def revoke(self, key): return requests.delete(f"{API}/licenses/{key}", headers=H, timeout=15).json() # delete forever
# ---- audit trail ----
def logs(self):
return requests.get(f"{API}/logs", headers=H, timeout=15).json()
# --- example: sell a key, then later blacklist it ---
v = Vshll()
new = v.create(count=1, expires_in_days=30, note="customer #4821")
key = new["licenses"][0]["license_key"]
print("give this to the buyer:", key)
# later, if they chargeback:
v.ban(key)
Discord bot (slash commands for your staff)
# pip install discord.py requests
# Minimal Discord bot: /genkey /ban /unban /reset /lookup
import discord, requests
from discord import app_commands
API = "https://vshll.lol/api/public/v1/admin"
SECRET = "YOUR_APP_SECRET_KEY"
H = {"Authorization": f"Bearer {SECRET}", "Content-Type": "application/json"}
ADMIN_ROLE_ID = 123456789012345678 # restrict to your staff role
bot = discord.Client(intents=discord.Intents.default())
tree = app_commands.CommandTree(bot)
def is_admin(inter): return any(r.id == ADMIN_ROLE_ID for r in inter.user.roles)
@tree.command(description="Generate a new license key")
async def genkey(inter: discord.Interaction, days: int = 30):
if not is_admin(inter): return await inter.response.send_message("Nope", ephemeral=True)
r = requests.post(f"{API}/licenses", headers=H, timeout=15,
json={"count": 1, "expires_in_days": days, "max_activations": 1}).json()
key = r["licenses"][0]["license_key"]
await inter.response.send_message(f"`{key}` ({days}d)", ephemeral=True)
@tree.command(description="Ban a license key")
async def ban(inter: discord.Interaction, key: str):
if not is_admin(inter): return await inter.response.send_message("Nope", ephemeral=True)
requests.post(f"{API}/licenses/{key}/ban", headers=H, timeout=15)
await inter.response.send_message(f"Banned `{key}`", ephemeral=True)
@tree.command(description="Clear all HWID activations on a key")
async def reset(inter: discord.Interaction, key: str):
if not is_admin(inter): return await inter.response.send_message("Nope", ephemeral=True)
requests.post(f"{API}/licenses/{key}/reset", headers=H, timeout=15)
await inter.response.send_message(f"Reset `{key}`", ephemeral=True)
@bot.event
async def on_ready(): await tree.sync(); print("ready")
bot.run("YOUR_DISCORD_BOT_TOKEN")
C++ client (libcurl + nlohmann/json)
// C++17 + libcurl + nlohmann/json
// vcpkg install curl nlohmann-json
#include <curl/curl.h>
#include <nlohmann/json.hpp>
#include <string>
#include <iostream>
using json = nlohmann::json;
static size_t write_cb(void* p, size_t s, size_t n, void* u) {
((std::string*)u)->append((char*)p, s*n); return s*n;
}
class Vshll {
std::string api = "https://vshll.lol/api/public/v1/admin";
std::string secret;
std::string req(const std::string& method, const std::string& path, const std::string& body = "") {
CURL* c = curl_easy_init(); std::string out;
curl_easy_setopt(c, CURLOPT_URL, (api + path).c_str());
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, method.c_str());
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, write_cb);
curl_easy_setopt(c, CURLOPT_WRITEDATA, &out);
curl_slist* h = nullptr;
h = curl_slist_append(h, ("Authorization: Bearer " + secret).c_str());
h = curl_slist_append(h, "Content-Type: application/json");
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
if (!body.empty()) curl_easy_setopt(c, CURLOPT_POSTFIELDS, body.c_str());
curl_easy_perform(c); curl_slist_free_all(h); curl_easy_cleanup(c);
return out;
}
public:
Vshll(std::string s): secret(std::move(s)) {}
json list() { return json::parse(req("GET", "/licenses")); }
json get(const std::string& k) { return json::parse(req("GET", "/licenses/" + k)); }
json create(int days = 30, int slots = 1) {
json b = {{"count", 1}, {"expires_in_days", days}, {"max_activations", slots}};
return json::parse(req("POST", "/licenses", b.dump()));
}
json ban(const std::string& k) { return json::parse(req("POST", "/licenses/" + k + "/ban")); }
json unban(const std::string& k) { return json::parse(req("POST", "/licenses/" + k + "/unban")); }
json reset(const std::string& k) { return json::parse(req("POST", "/licenses/" + k + "/reset")); }
json revoke(const std::string& k) { return json::parse(req("DELETE", "/licenses/" + k)); }
json logs() { return json::parse(req("GET", "/logs")); }
};
int main() {
Vshll v("YOUR_APP_SECRET_KEY");
auto r = v.create(30, 1);
std::cout << r["licenses"][0]["license_key"].get<std::string>() << "\n";
// v.ban("XXXX-XXXX-XXXX-XXXX-XXXX");
}
C# client (.NET 6+ HttpClient)
// .NET 6+ admin client. Build a WinForms/WPF/Avalonia dashboard,
// a console tool, or wire it into your store backend.
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
public class Vshll {
const string API = "https://vshll.lol/api/public/v1/admin";
readonly HttpClient http;
public Vshll(string secretKey) {
http = new HttpClient { BaseAddress = new Uri(API + "/") };
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", secretKey);
}
public Task<JsonElement> ListLicenses() => Get("licenses");
public Task<JsonElement> GetLicense(string k) => Get($"licenses/{k}");
public Task<JsonElement> Logs() => Get("logs");
public Task<JsonElement> Create(int count = 1, int expiresInDays = 30,
int maxActivations = 1, string? note = null) =>
Post("licenses", new { count, expires_in_days = expiresInDays,
max_activations = maxActivations, note });
public Task<JsonElement> Ban(string k) => Post($"licenses/{k}/ban", null);
public Task<JsonElement> Unban(string k) => Post($"licenses/{k}/unban", null);
public Task<JsonElement> Reset(string k) => Post($"licenses/{k}/reset", null);
public Task<JsonElement> Revoke(string k) => Delete($"licenses/{k}");
async Task<JsonElement> Get(string p) =>
await (await http.GetAsync(p)).Content.ReadFromJsonAsync<JsonElement>();
async Task<JsonElement> Post(string p, object? body) =>
await (await http.PostAsJsonAsync(p, body ?? new {})).Content.ReadFromJsonAsync<JsonElement>();
async Task<JsonElement> Delete(string p) =>
await (await http.DeleteAsync(p)).Content.ReadFromJsonAsync<JsonElement>();
}
// --- usage ---
// var v = new Vshll("YOUR_APP_SECRET_KEY");
// var made = await v.Create(count: 1, expiresInDays: 30, note: "customer #4821");
// Console.WriteLine(made.GetProperty("licenses")[0].GetProperty("license_key"));
// await v.Ban("XXXX-XXXX-XXXX-XXXX-XXXX");
secret_key bypasses the end-user API and can mint/ban/delete keys at will. Keep it on a server or local machine you control. If it ever leaks, rotate it from the dashboard immediately.Tips for production
- Always verify the returned
sha256before replacing the binary — protects against tampering. - If
mandatory: true, block app launch until the update finishes. - Run the update check on a background thread so the UI stays responsive.
- The signed download URL expires in 5 minutes — download immediately, don't cache it.
- For macOS/Linux replace the
.batswap with a shell script (mv+chmod +x+exec).
- Permissions: if the exe lives in
Program Files, the swap needs admin rights. Install per-user (%LOCALAPPDATA%) or ship a UAC manifest. - Antivirus: Defender / 3rd-party AV often blocks freshly-downloaded exes silently. Whitelist your publisher or sign the binary.
- File lock race: the updater above waits for the running PID to exit before
move— older versions using a fixedpingdelay would fail on slower disks. - Stale
version.txt: if it contains the new version but the exe is still old (failed mid-update), delete it and the app will re-download on next launch.