UMBRA KRATOS
Documentation

Building on Umbra.

For approved developers (resellers). It covers the SDK, how your product activates the hypervisor itself, licence linking, billing and the reseller API. Consumers need none of this: setup asks for the key and the console does the rest. For what the hypervisor itself can do, see Features. Supported guests: Windows 10 22H2 and Windows 11 25H2, 64-bit, on AMD processors with SVM and the IOMMU enabled.

The SDK

Your download is turinghv.dll, its header turinghv.h and an import library. The header is the only one you include; the DLL translates its stable C API to whatever hypervisor build is installed, so your binary keeps working across Umbra updates. The current API version is 1.7 (THV_CLIENT_API_VERSION 0x00010007).

#include <turinghv.h>

thv_connection_info info;
uint32_t rc = thv_open(THV_OPEN_DEFAULT, &info);
if (rc != THV_OK) {
    printf("umbra: %s\n", thv_status_string(rc));   /* e.g. not running, not trusted */
    return 1;
}
thv_cr3_query q = { THV_CR3_ACCEPT_ALL, pid };
thv_cr3_result r;
thv_query_cr3(&q, &r);
uint8_t buf[4096]; uint32_t got = 0;
thv_read_virtual(r.cr3, address, buf, sizeof buf, &got);
thv_close();

The caller gate

The hypervisor serves only programs it trusts: a measured binary the machine’s owner approved, running as a clean, elevated process. Ask for trust once with thv_trust_request(); the owner approves it, and from then on thv_open gets a session. When a call is refused, thv_get_access() says exactly why (THV_E_NOT_TRUSTED, THV_E_NOT_ELEVATED, THV_E_NOT_CLEAN, …). Rebuilding your binary changes its measurement, so ship an updated build and let it ask again.

What is in the API

AreaFunctions
Connectionthv_open, thv_close, thv_get_status, thv_get_access, thv_status_string
Memorythv_read_virtual, thv_write_virtual, thv_read_batch, thv_write_batch, thv_read_physical_batch, thv_get_physical_ranges
Address spacesthv_query_cr3, thv_cr3_scan_step, thv_slat_query
Systemthv_enumerate_processes, …_threads, …_user_modules, …_kernel_modules, …_memory, thv_get_process_info
Watchesthv_xwatch_* (execution), thv_wwatch_* (writes)
Trustthv_trust_request, thv_trust_query, thv_trust_revoke
Diagnosticsthv_log_read, thv_get_intro_diagnostics

Activating Umbra from your product

Your product activates the hypervisor itself — your customers never open UMBRA, and you do not need to ship it. The hypervisor holds its own licence: it builds the encrypted activation request inside itself and checks our signed answer itself. Windows only carries the message, through the small agent C:\turing-hv\hv.exe that the Umbra runtime installs. Your product hands that agent a key; that is the whole integration.

When to call it

The key is saved sealed to that machine, and the agent service activates it again at every boot and checks in every minute, so your product does not need to call it on every launch. No administrator rights are needed: an ordinary process can call it.

The call

Run hv.exe login, write the key and a newline to its standard input, close the input, and read one line of JSON from its output. Never pass the key as an argument: command lines are readable by every process on the machine, so hv.exe refuses keys there. It is a windowless program — always redirect its output.

// C++ (Win32). Returns hv.exe's exit code; 0 = activated and saved.
#include <windows.h>
#include <string>

int umbra_login(const std::string &key, std::string &json)
{
    SECURITY_ATTRIBUTES sa = { sizeof sa, nullptr, TRUE };
    HANDLE in_r, in_w, out_r, out_w;
    CreatePipe(&in_r, &in_w, &sa, 0);   SetHandleInformation(in_w, HANDLE_FLAG_INHERIT, 0);
    CreatePipe(&out_r, &out_w, &sa, 0); SetHandleInformation(out_r, HANDLE_FLAG_INHERIT, 0);

    STARTUPINFOW si = { sizeof si };
    si.dwFlags = STARTF_USESTDHANDLES;
    si.hStdInput = in_r; si.hStdOutput = out_w; si.hStdError = out_w;
    wchar_t cmd[] = L"\"C:\\turing-hv\\hv.exe\" login";
    PROCESS_INFORMATION pi = {};
    if (!CreateProcessW(nullptr, cmd, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi))
        return -1;
    CloseHandle(in_r); CloseHandle(out_w);

    std::string line = key + "\n";
    DWORD n;
    WriteFile(in_w, line.data(), (DWORD)line.size(), &n, nullptr);
    SecureZeroMemory(&line[0], line.size());
    CloseHandle(in_w);                                   // end of input: hv.exe reads one line

    char buf[4096];
    while (ReadFile(out_r, buf, sizeof buf, &n, nullptr) && n) json.append(buf, n);
    CloseHandle(out_r);
    WaitForSingleObject(pi.hProcess, 90000);
    DWORD code = 7;
    GetExitCodeProcess(pi.hProcess, &code);
    CloseHandle(pi.hProcess); CloseHandle(pi.hThread);
    return (int)code;
}
// C#
using System.Diagnostics;
using System.Text.Json;

static (int Code, JsonElement Result) UmbraLogin(string key)
{
    var psi = new ProcessStartInfo(@"C:\turing-hv\hv.exe", "login") {
        RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true,
        UseShellExecute = false, CreateNoWindow = true };
    using var p = Process.Start(psi)!;
    p.StandardInput.WriteLine(key);          // stdin, never an argument
    p.StandardInput.Close();
    string json = p.StandardOutput.ReadToEnd();
    p.WaitForExit();
    return (p.ExitCode, JsonDocument.Parse(json).RootElement);
}

var (code, r) = UmbraLogin(linkKey);
if (code != 0)
    ShowError(r.GetProperty("reason").GetString() is { Length: > 0 } why ? why : r.GetProperty("code_name").GetString());
# Python
import json, subprocess

def umbra_login(key):
    p = subprocess.run([r"C:\turing-hv\hv.exe", "login"], input=key + "\n",
                       capture_output=True, text=True, timeout=90)
    return p.returncode, json.loads(p.stdout)

Reading the answer

One JSON object. Ignore fields you do not know; new ones get added.

{"command":"login", "ok":true, "code":0, "code_name":"ok",
 "licensed":true, "reason":"", "http_status":200, "key_saved":true, ...}
Exit codeMeaningWhat your product should do
0Activated and savedDone. The hypervisor stays licensed across restarts.
1The hypervisor is not runningAsk the customer to restart (first install), then call again.
2No key was readYou sent an empty line, or a key longer than 62 characters.
3Could not reach the licence serverNetwork trouble; try again later. Nothing was saved.
4The licence server refused the keyShow reason — one of the refusal codes.
5The server’s reply did not verifySomething between the machine and us forged or altered the answer. Treat it as an attack, not a glitch.
6The hypervisor did not answer in timeTry again.
7Internal errorA Windows call failed; try again, then contact us.

Status, and signing out

hv.exe status answers the same way without changing anything: licensed says whether the hypervisor is licensed right now, runtime_code whether it is running at all. Use it to show “Umbra active” in your own UI. When your customer signs out of your product or moves to a new PC, run hv.exe forget to remove the saved key, and release the machine with reset-devices so the seat is free for the next one.

Keys

KindLooks likeWho makes it
ConsumerUMBR-7KQ2-M9XD-4HTW-PZ3CUs, when a subscription starts
Reseller keyACME-7KQ2-M9XD-4HTW-PZ3CYou, in the panel or over the API. Prefix = your code.
Link keyLK1.ACME.CUST42.20271231.1.5Q3NRA7XKX2M4WZ6BDFJYour licence server, from your link secret

Reseller keys carry a machine limit and an expiry: a fixed date, a number of days from first activation, or none (until you revoke it — flat plan and contracts only). What a key costs depends on how you pay; see Billing.

Licence linking

Your customers already have your licence. Linking lets that licence switch on the hypervisor, so they never see a second key:

  1. Your customer enters your key, once, in your product. Your server checks it, as it does today.
  2. Your server signs a short link key for that customer and returns it to your product.
  3. Your product gives the link key to the hypervisor (above). Our server checks your signature, creates the licence on first use, and activates.

Format

LK1.<code>.<ref>.<yyyymmdd>.<machines>.<mac>

code      your 4-character reseller code
ref       your reference for the customer: 1-16 of A-Z 0-9 _ -  (hash longer ids)
yyyymmdd  last valid day, UTC; at most 400 days ahead
machines  1-99
mac       first 20 characters of BASE32( HMAC-SHA256( link_secret, everything before the last dot ) )

Everything is upper case. The link secret is in your panel (Licence linking). Keep it on your server only — anyone holding it can mint licences in your name. Rotating it invalidates every link key signed with the old one.

Reference implementations

# Python
import base64, hashlib, hmac
def link_key(code, secret, ref, last_day, machines=1):
    signed = f"LK1.{code}.{ref.upper()}.{last_day:%Y%m%d}.{machines}"
    mac = base64.b32encode(hmac.new(secret.encode(), signed.encode(), hashlib.sha256).digest()).decode()[:20]
    return f"{signed}.{mac}"
// Node.js
const crypto = require("crypto");
function linkKey(code, secret, ref, yyyymmdd, machines = 1) {
  const signed = `LK1.${code}.${ref.toUpperCase()}.${yyyymmdd}.${machines}`;
  const mac = base32(crypto.createHmac("sha256", secret).update(signed).digest()).slice(0, 20);
  return `${signed}.${mac}`;
}
function base32(buf) {  // RFC 4648 alphabet, no padding
  const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = 0, val = 0, out = "";
  for (const b of buf) { val = (val << 8) | b; bits += 8; while (bits >= 5) { out += A[(val >>> (bits - 5)) & 31]; bits -= 5; } }
  if (bits > 0) out += A[(val << (5 - bits)) & 31];
  return out;
}

Would rather not implement it? POST /api/v1/reseller/link-keys signs one for you (below). Either way your server stays the authority on who is licensed: issue link keys that expire when your customer’s licence does, and renew them as you renew it. To cut a customer off at once, revoke their reference.

Billing

Choose in your panel, under Billing, and switch whenever you like:

PlanYou payBest when
Pay as you goLicence credits bought in packs at 35–65% off retail. One credit is one machine for 30 days: a 90-day key for two machines costs 6. Credits never expire.You are starting, or your volume moves around
Flat plan$1,000 a month for up to 100 active licences, keys and linked licences alike. No credits.You keep a steady 60–100 customers active and want a predictable bill
ContractBy agreement, invoicedLarger volumes — ask us

Reseller API

Base URL https://api.ringminusone.com/api/v1/. Authenticate every request with an API key from your panel: Authorization: Bearer rk_…. Bodies and replies are JSON. Server-to-server only — never put an API key in software you ship.

RequestDoes
GET reseller/usageActive licences, keys, linked licences, machines, billing mode, credit balance, licence cap
POST reseller/licensesMint keys. {"count":10,"max_devices":1,"duration_days":30} or "expires_on":"2027-06-30"; optional note. With "external_ref" and count 1 it is idempotent: the same ref returns the same key.
GET reseller/licenses?status=&batch=&external_ref=&page=List your keys, 100 per page
GET / PATCH reseller/licenses/<key>One key with its machines; PATCH max_devices, expires_on (null = never), note
POST reseller/licenses/<key>/<action>revoke, suspend, restore, reset-devices
POST reseller/link-keys{"ref":"CUST42","expires_on":"2027-12-31","devices":1} → {"link_key":"LK1…"}
GET reseller/links/<ref>A linked licence and its machines (exists after first use)
POST reseller/links/<ref>/<action>revoke, suspend, restore, reset-devices
curl -s -X POST https://api.ringminusone.com/api/v1/reseller/licenses \
  -H "Authorization: Bearer rk_..." -H "Content-Type: application/json" \
  -d '{"count": 5, "max_devices": 1, "duration_days": 30, "note": "May giveaway"}'

Errors are {"error": "<code>", "detail": "…"} with a 4xx status: unauthorized, insufficient_credits (402), quota_exceeded (409, your plan’s licence cap), invalid, not_found, throttled (429; 600 requests per minute per key).

Client and branding

In the panel, choose what your customers see: nothing (the default — your product is the UI), a tray icon, or a minimal window showing the hypervisor’s state and the licence. Both are one small program, umbra-client.exe (about 180 KB, no runtime to install). Download it from your panel already configured: a zip with the exe and an umbra-client.ini holding your code. Copy both into your product’s folder and start the exe at sign-in with a Run key (HKCU\Software\Microsoft\Windows\CurrentVersion\Run). It reads the hypervisor’s state through hv.exe status every few seconds and never touches the hypervisor itself.

With the white-label add-on, set your product name, logo and colours. The client reads them from a public endpoint and caches them, so a change needs no new build:

GET https://api.ringminusone.com/api/v1/client/<code>/config
{"vendor": "Acme Debugger", "code": "ACME", "mode": "tray",
 "brand": {"name": "Acme Debugger", "logo": "https://…/brand/acme.png", "accent": "#3B82F6", "background": "#0B1020"}}

Refusal codes

When activation is refused, hv.exe login exits non-zero and the JSON line it prints carries the server’s reason. The codes:

CodeMeaning
not_foundNo such key, or a link key whose signature does not check out
expiredThe key or link key is past its last day
device_limitAlready active on its maximum number of machines (release one in the panel)
revoked / suspendedYou or we switched it off
quota_exceededA first-use link key would go over your plan’s licence cap
vendor_out_of_creditPay as you go: a linked licence started a new 30-day period and your credit balance could not cover it
reseller_inactiveYour account is suspended, linking is switched off in your panel, or the flat plan a key was made on has lapsed
subscription_inactiveConsumer keys: the subscription lapsed
unknown_deviceA heartbeat from a machine that was released — it activates again on next boot if a seat is free