Skip to content
RudeAuth Docs

Documentation

Every response RudeAuth sends is signed. Your binary carries a public key that can verify a reply but never forge one — so a patched server, a proxy, or a hosts-file redirect cannot fake a successful licence check.

Base URL: https://api.rudeauth.com · Dashboard: app.rudeauth.com

Quickstart

Four steps from nothing to a working licence check.

1. Create an application

In the dashboard, open Applications and create one. It is issued its own Ed25519 signing keypair. Copy the application ID and the public key — both are embedded in your binary, and neither is a secret.

2. Generate licence keys

Licences → Generate. Choose how many, how many days they last (0 for perpetual) and how many devices each key may bind to.

Keys are displayed once. Only a peppered HMAC is stored, so nobody — including you, including anyone who steals the database — can read them back afterwards. Export the batch before leaving the page.

3. Integrate the SDK

// The public key ships with your binary. It can verify a reply,
// never forge one.
rudeauth::Client client{APP_ID, PUBLIC_KEY};

auto session = client.redeem(userKey);
if (!session) return 1;  // no bool to patch

4. Verify it refuses

Revoke the licence in the dashboard and run your software again. It should be refused immediately. A licensing system you have only watched succeed is one you have not tested.

How verification works

A client never trusts the network. Instead of asking "am I licensed?" and believing the answer, it checks a signature it can verify offline.

Verifying a response

Every endpoint returns the same envelope:

{
  "data": "<base64 of the JSON payload>",
  "signature": "<base64 Ed25519 signature>"
}

Verify the signature over the raw decoded bytes of data, using your application's public key, before parsing anything inside it. Parsing first and verifying second defeats the purpose.

The signature is domain-separated by endpoint name, so a valid handshake response cannot be presented as a files response.

The SDK does this for you. If you are writing your own client, it is the one part you must not skip: an unverified envelope is just JSON from the network.

One exception. A request that cannot be attributed to an application — a malformed body, or an unknown app_id — is answered with an unsigned {"error":"BAD_REQUEST"} and HTTP 400. There is no signing key to sign it with. Treat any unsigned response as a failure.

Handshake

POST/v1/handshake

Redeems a licence key, binds the device, and opens a session. Called once per launch.

Request

FieldTypeNotes
app_idstringUUID from the dashboard
app_versionstringYour build version, recorded in the audit log
license_keystringThe customer's key
fingerprint_componentsstring[]Hardware identifiers. Maximum 16 — see below
fingerprint_labelstringHuman-readable device name, shown in the dashboard
client_noncestringbase64. Fresh random bytes per request
eph_pubkeystringbase64. 32-byte X25519 public key
sent_atint64Unix seconds. Must be within the clock-skew window

Response payload

{
  "success": true,
  "client_nonce": "<echoed back>",
  "server_time": 1788134400,
  "session_token": "<base64>",
  "session_expires_at": 1788135300,
  "session_id": "<uuid>",
  "server_eph_pubkey": "<base64>",
  "license": {
    "level": 1,
    "expires_at": 1790726400,
    "devices_used": 1,
    "max_devices": 3
  }
}

Derive the session key with X25519 against server_eph_pubkey, using session_id as HKDF info. The server never stores your ephemeral private key.

On failure, success is false and error holds one of the error codes. The response is still signed.

Heartbeat

POST/v1/heartbeat

Confirms a session is still valid and extends it. Call periodically if your software runs for a long time — revoking a licence kills its sessions, and the heartbeat is how a running client learns that.

FieldType
app_idstring
session_tokenstring (base64)
{ "success": true, "valid": true, "server_time": 1788134400, "expires_at": 1788135300 }

valid: false means the session was revoked or expired. Stop the protected functionality — do not retry until the next handshake.

Variables

POST/v1/variables

Fetches server-side values, sealed to the session.

FieldType
app_idstring
session_tokenstring (base64)

Returns sealed: base64 ciphertext, opened with the session key.

Know the limit. A variable reaches the client, so a determined user can read it out of memory. Variables are worth using for values that rotate — an endpoint that moves, a key that changes weekly — not for a secret that must never be seen.

Files

POST/v1/files

Delivers an encrypted payload that never shipped inside your binary. The strongest gate available: a customer without a valid licence never receives the bytes at all.

FieldType
app_idstring
session_tokenstring (base64)
namestring — as uploaded

Returns sealed and version. An unknown name returns FILE_NOT_FOUND, which is distinct from a licence failure — check for it explicitly rather than reporting "invalid licence" for a typo in a filename.

Device reset

POST/v1/device/reset

Lets a customer unbind their own devices, subject to the cooldown and lifetime cap you set. This is the single most common support request any licensing system receives — a customer replaced a motherboard — and exposing it in your software saves you answering it.

FieldType
app_idstring
license_keystring

Returns RESET_UNAVAILABLE when the cooldown has not elapsed or the lifetime cap is spent. An operator can always reset from the dashboard, which bypasses both.

Webhook

POST/v1/webhook

Invokes an endpoint you configured as client callable, through RudeAuth. Your client never learns the destination URL, and the outbound request is SSRF-guarded and never follows redirects.

FieldType
app_idstring
session_tokenstring (base64)
namestring — the endpoint's name
paramsobject — string to string

Endpoints not marked client-callable cannot be reached this way; they only receive events RudeAuth sends.

Error codes

Returned in the signed payload's error field. Match on the code, never the message — messages are for humans and may change.

CodeMeaningWhat the client should do
LICENSE_INVALIDUnknown, malformed or revoked keyAsk for the key again
LICENSE_EXPIREDThe subscription has run outPoint at your renewal page
DEVICE_LIMITAll device slots are in useOffer device reset
DEVICE_BLACKLISTEDThis hardware is bannedStop. Do not retry
SESSION_EXPIREDThe session lapsed or was revokedHandshake again
CLOCK_SKEWsent_at is too far from server timeTell the user to fix their clock
RATE_LIMITEDToo many attemptsBack off, then retry
APP_DISABLEDThe application is paused or unknownStop. Contact the vendor
ENDPOINT_DISABLEDThat endpoint is off for this appStop calling it
FILE_NOT_FOUNDNo file by that nameCheck the name — not a licence problem
RESET_UNAVAILABLECooldown or lifetime capTell the user when they may retry
QUOTA_EXCEEDEDThe vendor's plan limit is reachedVendor action, not the customer's
SERVER_ERRORSomething failed our sideRetry with backoff

Deliberate ambiguity. A missing application and a disabled one both return APP_DISABLED; an unknown key and a malformed one both return LICENSE_INVALID, and take the same code path so timing does not distinguish them either. Precise errors here would be a free oracle for anyone enumerating keys.

Device fingerprints

Send several independent identifiers. RudeAuth hashes each one before it touches the database — raw values are never stored.

Useful components: CPU ID, motherboard serial, disk serial, MAC address, machine GUID.

Avoid values shared across machines. A virtualised disk serial can be identical on thousands of unrelated PCs — bans on such a component reach far beyond the machine you meant, which is why the dashboard warns before letting you do it.

Rate limits

Limits apply per application and per source address. Exceeding one returns RATE_LIMITED.

A normal client handshakes once per launch and heartbeats occasionally, so it will not come close. If you are hitting the limit, something is retrying in a loop — fix the retry, do not raise the ceiling.

Back off exponentially. Retrying immediately makes it worse for every one of your customers.


Something unclear or wrong here? It is a bug in the documentation, and worth reporting as one.