Skip to documentation
RudeAuth Documentation

Search the documentation

Jump to a section, an endpoint, or an error

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
Integration
Four checkpoints

Quickstart

Four steps from nothing to a working licence check.

Copy-paste this prompt into Claude Code, Cursor, or any LLM and it integrates RudeAuth the right way. It reads the rules at /llms.txt and each SDK's AGENTS.md, so it will not reach for the patchable bool is_licensed() pattern.

AI prompt
Add RudeAuth licensing to my <language> app.
Read https://docs.rudeauth.com/llms.txt for how RudeAuth works, and follow the
rudeauth-<lang> SDK's AGENTS.md rules: no bool is_licensed(), embed the public key,
verify before trust, no offline cache, and gate real logic into a server-delivered
payload. My app id is <APP_ID> and my public key is <PUBLIC_KEY>.
Checkpoint 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.

DashboardApplicationsCreate application
Checkpoint 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 here, and looked up by a peppered HMAC. A copy sealed under your master key is kept so you can export a batch or re-reveal a single key later; both are written to the audit log. The master key lives in the environment, not the database, so a stolen database is a list of digests and ciphertext on its own.

Checkpoint 3

Integrate an SDK

RudeAuth ships official SDKs for five languages. Each verifies every response against the embedded public key before trusting a field, and none exposes a bool is_licensed() for a patch to flip. Pick yours:

From source · github.com/Rudevin17/rudeauth-cpp · C++17 static library
#include <rudeauth/rudeauth.hpp>

rudeauth::Client client(APP_ID, PUBLIC_KEY, "https://api.rudeauth.com");

auto auth = client.authenticate(userKey);
if (!auth.ok()) return 1;          // no bool to patch
auto session = auth.value();
dotnet add package RudeAuth · source
using RudeAuth;

using var client = new RudeAuthClient(appId, publicKey, "https://api.rudeauth.com");
using var session = client.Authenticate(userKey);  // throws RudeAuthException on failure
pip install rudeauth · source
from rudeauth import Client, RudeAuthError

client = Client(app_id, public_key, "https://api.rudeauth.com")
session = client.authenticate(user_key)   # raises RudeAuthError on failure
go get github.com/Rudevin17/rudeauth-go
import "github.com/Rudevin17/rudeauth-go"

client, err := rudeauth.NewClient(appID, publicKey, "https://api.rudeauth.com")
if err != nil { return err }

sess, err := client.Authenticate(userKey)  // sentinel errors, e.g. rudeauth.ErrLicenseExpired
if err != nil { return err }
defer sess.Close()
cargo add rudeauth · source
use rudeauth::Client;

let client = Client::new(app_id, public_key, "https://api.rudeauth.com")?;
let session = client.authenticate(user_key)?;

APP_ID and the public key come from your application in the dashboard. Both are safe to embed: the public key verifies responses and cannot forge them.

Check your integration

The rules above are only worth anything if something checks them. The CLI does, locally:

rudeauth-cli review ./src

It exits 1 when something matched and 0 when nothing did, so it works as a CI step with no wrapper around it. It needs no token, no database and no network, because it has to run on a build machine that has never heard of RudeAuth.

CheckWhy it matters
bool-gateThe whole entitlement becomes one branch, and a patched binary only has to invert it
key-from-configA public key read at runtime is a public key an attacker can swap for their own
offline-fallbackA cache is exactly what an attacker induces by blocking the network
generic-errorExpired, device-limit, banned and revoked need different answers, and one catch-all tells the user none of them

Your source never leaves your machine. There is no upload, no telemetry and no key. Asking for your code in exchange for a lint result would be a poor trade for you and a liability for us to hold, so the check runs where the code already is.

It cannot tell you an integration is sound, and it does not claim to. It matches known patterns rather than parsing your program, so a clean run means those patterns were not found, not that nothing is wrong. Two of the rules are not here at all: whether you gated real logic rather than a splash screen, and whether your own copy overclaims, both need judgment. Point an assistant at llms.txt and your SDK's AGENTS.md for those.

Go and C++ today. The other three SDK languages follow once the rule set has settled, because a check that cries wolf is one people learn to skip.

Checkpoint 4 · Security drill

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.

1

Revoke the test keyDashboard → Licences → Revoke

2

Run the client againDo not change the local key or the device

3

Expect a typed refusalThe response is signed; no session is issued

Decision
REFUSED
Trust result
Nothing released
Recovery
Use a valid licence or restore access

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.

  • Signed responses. Every reply is Ed25519-signed by your application's key. The signature covers the exact response bytes and the endpoint name, so a reply from one endpoint cannot be replayed as another's.
  • Device binding. A licence binds to hardware using K-of-N matching, so replacing a disk does not lock your customer out, but copying the key to another machine does not work.
  • Replay protection. Each handshake carries a client nonce, remembered server-side for the length of the clock-skew window. Capturing a successful response and replaying it fails.
  • Session keys. The handshake performs an X25519 exchange. Variables and files come back sealed to that session, so they are useless if captured.

Verifying a response

Every endpoint returns the same envelope:

  1. RequestFresh nonce and device proof
  2. EnvelopeData bytes and signature
  3. VerifyEd25519 public key
  4. ParseOnly after acceptance
{
  "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, listed below.

Events

An endpoint receives an event only if it is subscribed to it. Put the event names in the endpoint's Events field in the dashboard, comma separated. An endpoint with an empty Events field receives nothing and exists only to be called through the proxy above.

EventSent when
license.redeemedA handshake succeeded. Sent on every activation, not only the first
device.boundA handshake bound a machine that was not on the licence before
device.resetA licence's devices were unbound, whether by the customer or by you
license.bannedYou revoked a licence
license.expiredA licence passed its expiry. Sent once, when it lapses
license.renewedA licence was extended. The key, its devices and its sessions are unchanged
sharing.flaggedA licence was flagged as likely shared, or its risk evidence changed

Every body has the same envelope. data carries the fields for that event.

{
  "event": "device.bound",
  "at": "2026-08-21T09:14:22Z",
  "data": {
    "license_id": "8f1c...",
    "device_id": "2b40...",
    "devices_used": 2,
    "max_devices": 3
  }
}
FieldAppears in
license_idevery event
device_iddevice.bound
session_idlicense.redeemed
devices_used, max_deviceslicense.redeemed, device.bound
actordevice.reset, license.banned, license.renewed
expires_atlicense.renewed
level, scoresharing.flagged

A body carries ids and counts, never a licence key, a session token or a fingerprint. A webhook lands in your logs, and often in a third party's, so there is nothing in one worth stealing from there. To turn an id into detail, read it back through the Management API or the dashboard, both of which are authenticated.

Delivery retries with backoff for up to ten attempts. Respond 2xx to acknowledge; anything else is a retry. Treat events as at-least-once and make your handler idempotent, since a retry after a timeout you actually processed is indistinguishable from a first delivery.

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.

  • Minimum: set per application. A handshake with fewer is refused.
  • Maximum 16. More is rejected. Real fingerprints are a handful of values, and an unbounded list is an unbounded row.
  • Matching is K-of-N. Enough components must agree, not all of them, so a replaced disk does not lock a customer out.

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.