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.
- 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:
{
"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
| Field | Type | Notes |
|---|---|---|
app_id | string | UUID from the dashboard |
app_version | string | Your build version, recorded in the audit log |
license_key | string | The customer's key |
fingerprint_components | string[] | Hardware identifiers. Maximum 16 — see below |
fingerprint_label | string | Human-readable device name, shown in the dashboard |
client_nonce | string | base64. Fresh random bytes per request |
eph_pubkey | string | base64. 32-byte X25519 public key |
sent_at | int64 | Unix 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.
| Field | Type |
|---|---|
app_id | string |
session_token | string (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.
| Field | Type |
|---|---|
app_id | string |
session_token | string (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.
| Field | Type |
|---|---|
app_id | string |
session_token | string (base64) |
name | string — 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.
| Field | Type |
|---|---|
app_id | string |
license_key | string |
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.
| Field | Type |
|---|---|
app_id | string |
session_token | string (base64) |
name | string — the endpoint's name |
params | object — 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.
| Code | Meaning | What the client should do |
|---|---|---|
LICENSE_INVALID | Unknown, malformed or revoked key | Ask for the key again |
LICENSE_EXPIRED | The subscription has run out | Point at your renewal page |
DEVICE_LIMIT | All device slots are in use | Offer device reset |
DEVICE_BLACKLISTED | This hardware is banned | Stop. Do not retry |
SESSION_EXPIRED | The session lapsed or was revoked | Handshake again |
CLOCK_SKEW | sent_at is too far from server time | Tell the user to fix their clock |
RATE_LIMITED | Too many attempts | Back off, then retry |
APP_DISABLED | The application is paused or unknown | Stop. Contact the vendor |
ENDPOINT_DISABLED | That endpoint is off for this app | Stop calling it |
FILE_NOT_FOUND | No file by that name | Check the name — not a licence problem |
RESET_UNAVAILABLE | Cooldown or lifetime cap | Tell the user when they may retry |
QUOTA_EXCEEDED | The vendor's plan limit is reached | Vendor action, not the customer's |
SERVER_ERROR | Something failed our side | Retry 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.