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
| Area | Functions |
|---|---|
| Connection | thv_open, thv_close, thv_get_status, thv_get_access, thv_status_string |
| Memory | thv_read_virtual, thv_write_virtual, thv_read_batch, thv_write_batch, thv_read_physical_batch, thv_get_physical_ranges |
| Address spaces | thv_query_cr3, thv_cr3_scan_step, thv_slat_query |
| System | thv_enumerate_processes, …_threads, …_user_modules, …_kernel_modules, …_memory, thv_get_process_info |
| Watches | thv_xwatch_* (execution), thv_wwatch_* (writes) |
| Trust | thv_trust_request, thv_trust_query, thv_trust_revoke |
| Diagnostics | thv_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
- After the first restart. The hypervisor has to be running, because it is the one that talks to us. Before the restart,
loginanswers code 1, “the hypervisor is not running”. - After your own licence check succeeds, with our key — a reseller key, or a link key your server just signed.
- Whenever the key changes, for example when you renew a customer’s link key. Calling it again with the same key is harmless.
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 code | Meaning | What your product should do |
|---|---|---|
| 0 | Activated and saved | Done. The hypervisor stays licensed across restarts. |
| 1 | The hypervisor is not running | Ask the customer to restart (first install), then call again. |
| 2 | No key was read | You sent an empty line, or a key longer than 62 characters. |
| 3 | Could not reach the licence server | Network trouble; try again later. Nothing was saved. |
| 4 | The licence server refused the key | Show reason — one of the refusal codes. |
| 5 | The server’s reply did not verify | Something between the machine and us forged or altered the answer. Treat it as an attack, not a glitch. |
| 6 | The hypervisor did not answer in time | Try again. |
| 7 | Internal error | A 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
| Kind | Looks like | Who makes it |
|---|---|---|
| Consumer | UMBR-7KQ2-M9XD-4HTW-PZ3C | Us, when a subscription starts |
| Reseller key | ACME-7KQ2-M9XD-4HTW-PZ3C | You, in the panel or over the API. Prefix = your code. |
| Link key | LK1.ACME.CUST42.20271231.1.5Q3NRA7XKX2M4WZ6BDFJ | Your 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:
- Your customer enters your key, once, in your product. Your server checks it, as it does today.
- Your server signs a short link key for that customer and returns it to your product.
- 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:
| Plan | You pay | Best when |
|---|---|---|
| Pay as you go | Licence 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 |
| Contract | By agreement, invoiced | Larger volumes — ask us |
- Keys are paid for when you generate them. On pay as you go a key needs an expiry; it then runs to that expiry whatever happens to your balance.
- Linked licences are paid for as they are used: the first activation of each 30-day period takes the licence’s machine count in credits. With no credits left, new periods are refused with
vendor_out_of_credit— top up and they work again at the next check-in. - Flat plan keys and linked licences work while the plan is paid. If it lapses, its keys pause and pay as you go applies again.
- All payments are final; there are no refunds.
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.
| Request | Does |
|---|---|
GET reseller/usage | Active licences, keys, linked licences, machines, billing mode, credit balance, licence cap |
POST reseller/licenses | Mint 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:
| Code | Meaning |
|---|---|
not_found | No such key, or a link key whose signature does not check out |
expired | The key or link key is past its last day |
device_limit | Already active on its maximum number of machines (release one in the panel) |
revoked / suspended | You or we switched it off |
quota_exceeded | A first-use link key would go over your plan’s licence cap |
vendor_out_of_credit | Pay as you go: a linked licence started a new 30-day period and your credit balance could not cover it |
reseller_inactive | Your account is suspended, linking is switched off in your panel, or the flat plan a key was made on has lapsed |
subscription_inactive | Consumer keys: the subscription lapsed |
unknown_device | A heartbeat from a machine that was released — it activates again on next boot if a seat is free |