AuthGuard
Documentation

Integrate in minutes

Create an application in the dashboard, copy its credentials, drop in an SDK.

1. Get your credentials

Dashboard → your application → Integration. None of these are secret; they ship inside your program.

App ID
Identifies the application.
Public key
Verifies the server's signature. Also shown on the Integration tab.
API URL
https://authguard.dk/api/client

2. Verify on start-up

from authguard import AuthGuard, AuthGuardError

app = AuthGuard(app_id="YOUR_APP_ID", version="1.0", public_key="YOUR_PUBLIC_KEY")

try:
    app.init()                       # version check, opens a session
    app.license(key)                 # or app.login(user, password)
except AuthGuardError as e:
    if e.response.get("update_required"):
        print("Update at", e.response["download_url"])
    else:
        print("Denied:", e)
    raise SystemExit(1)

print("Expires:", app.info["expires_at"] or "never")

Call app.check() periodically while the program runs. It re-reads the licence, so a revoke or a ban also stops a session that is already open.

What this protects against, and what it does not

AuthGuard stops someone using a key they are not entitled to, and it stops a spoofed server: replies are signed with Ed25519, so a fake endpoint that answers "valid" is rejected by the SDK.

It does not make your program impossible to crack. The SDKs are ordinary source files that you compile into your build, so anyone who can modify that build can delete the licence call, skip the signature check, or make init() return early. No client-side licensing system can prevent this, and you should not promise your customers otherwise.

What actually raises the cost of cracking:

Client API

POST /api/client

Every request carries type, app_id and a random nonce. Everything after init also carries the sessionid that init returned. No API key is involved: this endpoint is meant to run inside your customers' software.

TypeFieldsNotes
init version, hwid Opens a session. Refuses a version that is not an exact match, with update_required and a download link.
license key, hwid Key-only access. The expiry clock starts here if the key has never been used.
register username, password, key, hwid Consumes an unused key and creates an account.
login username, password, hwid Account access.
var name Reads a server-side value. Protected ones need a verified session.
check — Re-reads the licence or account, so a revoke or ban stops a running program.

Responses and signatures

Every client reply is HTTP 200 with a signed envelope, including refusals and rate limits, so your software can always prove the answer came from AuthGuard.

{
  "payload": "{\"success\":true,\"message\":\"License valid\",\"info\":{...},\"nonce\":\"a1b2\",\"timestamp\":1792267300}",
  "signature": "4199c920a754a069..."
}

The signature is Ed25519 over the exact payload bytes. The SDKs reject a reply whose signature does not verify, whose nonce differs from the one they sent, or whose timestamp is more than five minutes from the local clock. If you write your own client, you must do all three: an unverified reply gives you no protection at all.

Failure handling

The SDKs fail closed. A network error, a timeout, an unverifiable signature, a wrong nonce or a timestamp outside the window all raise AuthGuardError. If you do not catch it, your program stops. That is the safe default, but it also means a customer with no internet cannot start your software.

Request timeout is 15 seconds in the Python and C# SDKs. There is no automatic retry and no built-in grace period; if you want one, decide the policy yourself:

try:
    app.init()
    app.license(key)
    save_last_success(time.time())        # remember a good check
except AuthGuardError as e:
    offline = "reach the license server" in str(e)
    # Allow a short offline window, but never for a refusal the server actually sent.
    if not offline or time.time() - last_success() > 3 * 86400:
        raise SystemExit("License check failed")

Only treat a connection failure as offline. A signed refusal — revoked, expired, banned, wrong machine — is a real answer from the server and should never fall into a grace period.

Clocks matter. The five-minute window is measured against the computer running your software. A machine whose clock is badly wrong will see "Invalid server response" even though nothing is wrong with the licence. The server itself works entirely in UTC. If you support machines with unreliable clocks, say so in your own error message so the user knows to check the date.

Machine IDs

The SDKs build the hwid themselves and send it with the request. They take a stable identifier from the operating system and hash it with SHA-256:

Windows
HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
macOS
IOPlatformUUID
Linux
/etc/machine-id
Fallback
A MAC-address-derived value, if none of the above can be read.

Because the value is produced on the customer's computer and sent in the request, the server cannot verify it, and anyone who edits the SDK can send whatever they like. Treat it as a deterrent against a key being shared with friends, not as proof of identity. Reinstalling an operating system usually changes it, which is why the dashboard and the bot can unbind a key.

Sessions

init returns a 64-character random sessionid. It is stored server-side against the application and expires 30 minutes later, or 6 hours after a successful license, login or register.

The session is bound to the random token alone — not to the IP address and not to the machine ID. A session identifier copied to another computer works until it expires. Keep it in memory, do not log it and do not write it to disk.

Variables

Values your program fetches at runtime instead of hard-coding them. Each one is either protected or open:

SettingWho can read it
Require loginOnly a session that has passed license, login or register.
OpenAny session that completed init, which needs only your App ID and the current version.

Your App ID is inside every copy of your program, so an open variable is effectively public: assume anyone who has your software can read it. Put anything that matters behind Require login, which is the default when you create one.

Licence lifecycle

Expiry starts on first use, not on creation. A key issued with 30 days sits unused indefinitely; the clock starts the first time it is accepted.

Created
Status unused, no expiry date, valid forever until used.
First accepted
Expiry is set to that moment plus the key's days, and the machine is bound.
Never used
Stays unused and never expires on its own. Delete it if you want it gone.
No days set
A lifetime key: it activates and binds, but never expires.

register consumes the key. On success the key is marked used, tied to the new account, bound to that machine, and the subscription end is calculated then. The account and the key are written in one database transaction with a guard on the key, so two people redeeming the same key at the same moment cannot both succeed, and a failure part-way writes nothing at all. A key that was already used — by license or by register — is refused with "This license key has already been used", and a key tied to an account can no longer be used as a plain key.

Version checks

The version your software sends to init is compared to the application's Current version as an exact string match. There is no numeric or semantic comparison: 1.10 and 1.9 are simply two different strings, and 1.0 does not match 1.0.0.

Anything that is not an exact match is refused with update_required and the download URL, including a version newer than the dashboard value. Update the dashboard first, then release, or your own new build will be locked out.

Rate limits

Counted per IP address in fixed windows.

EndpointLimitHow it arrives
POST /api/client120 per minute per IPSigned envelope with success false and rate_limited true
POST /api/v1/validate60 per minute per IPHTTP 429 with {"detail": "..."}
POST /api/auth/login10 per minute per IPHTTP 429 with {"detail": "..."}
POST /api/auth/register5 per hour per IPHTTP 429 with {"detail": "..."}

The client API never answers with a bare 429, because an SDK cannot verify an unsigned reply and would report it as an unreachable server. It returns a signed refusal carrying rate_limited instead.


The host throttles bursts too

Separately from the limits above, the hosting platform protects the site at its edge, before a request ever reaches AuthGuard. In testing, around 50 requests in quick succession from one IP address triggered it, and the answer was an HTML page rather than a signed envelope. The SDKs cannot verify that, so they report it as an unreachable server and fail closed.

Verify once at start-up and then only occasionally — minutes apart, not seconds. Never poll in a tight loop. Remember that an office or school behind one shared address spends that budget together, so a handful of your customers on the same network can trip it for all of them.

Management API

X-API-Key

For the Discord bot and your own server-side scripts. Send your dashboard API key as the X-API-Key header.

GET /api/products your applications
POST /api/licenses issue keys
GET /api/licenses?q= search your keys
POST /api/licenses/{key}/revoke revoke
POST /api/licenses/{key}/reset-hwid unbind machine
POST /api/v1/validate unsigned check, server-side only
DELETE /api/licenses/{key} delete

About your API key

  • One key per account. It has full access to that account: every application, key, end user, variable and blacklist entry.
  • There are no narrower scopes and no IP restriction, so treat it like a password and keep it on servers you control. Never ship it inside your software.
  • Rotating it in the dashboard invalidates the old key immediately. Anything still using it, including the Discord bot, stops until you update it.

/api/v1/validate

A plain unsigned yes-or-no check. It requires your API key and only ever sees keys belonging to your own applications. Because the reply is not signed, it is for server-side use only — the Discord bot, a web shop, your own backend. Never call it from software you ship: a client that trusts an unsigned answer can be pointed at any server that says "valid". Use the signed client API there instead.

Errors

StatusMeaning
401Missing or wrong credentials.
403Authenticated, but not allowed.
404Not found, or not owned by your account.
409Already exists.
422Input failed validation.
429Rate limited.

These apply to the management and account endpoints, which answer with {"detail": "..."}. Client API refusals are not HTTP errors: they arrive as HTTP 200 inside the signed envelope with success set to false.

SDK downloads

Version 1.0

There is no package on PyPI or NuGet yet, so these are source files you add to your project. Check the SHA-256 after downloading; the values below are computed from the files this server is serving right now.

They are readable source, not compiled libraries, and they stay readable once they are inside your program — so assume your customers can read the licence logic too. That is inherent to a client-side SDK and is exactly why the limits section matters. Nothing secret lives in them: your App ID and public key are identifiers, not credentials, and your account API key never belongs anywhere near shipped software.

Python 3.9+

requests>=2.32,<3 cryptography>=43,<47
authguard.py 2034f73598b6eebf22aa07d8d9b1b88ab9d76bea4a20a3a76cfbdb25d344c679
example.py 47c5618b36332721ce0db77ece447ef8ca853e06e1f561a05e3b050b9d42e95d

C# / .NET 6+

dotnet add package NSec.Cryptography --version <the version you tested>
AuthGuard.cs 7de650a20c20cb820d9021347d2adc5c4dc84d7a236c648f8ec9fbc1fb09d228
Program.cs 8f9682142abb2bdf1e285725b727c48dbcdbcfaaa897a68fb2b6f62416d988a7

C++17, Windows

vcpkg install libsodium nlohmann-json
authguard.hpp 02b42f0e981e6e528df5a335fcb0b321cabdedb912be762d76c9f9e39de7b484
main.cpp 164143dbbc4f448793d4fe44b56eef253accefc76e48c9134ecd094984a8723e

Verify with sha256sum authguard.py on Linux or macOS, or certutil -hashfile authguard.py SHA256 on Windows.

Versioning

AuthGuard is in beta. The client API shape described on this page — the request fields, the signed envelope and the refusal messages — is what the service does today, and any change to it will be described here. The SDKs are version 1.0 and identify themselves with a User-Agent of AuthGuard-SDK/1.0; pin the files by the checksums above rather than re-downloading them blindly into a build.

Questions, or something on this page that does not match what you see? Email support@authguard.dk.