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:
- Keep something the program genuinely needs on the server and fetch it with variables, so a patched client is missing real functionality rather than just a check.
- Call check() regularly, not only at start-up, and from more than one place in the code.
- Ship compiled or obfuscated builds. Plain Python scripts are the easiest target of all.
- Treat machine IDs as a deterrent against casual key sharing, not as proof of identity.
Client API
POST /api/clientEvery 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.
| Type | Fields | Notes |
|---|---|---|
| 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:
| Setting | Who can read it |
|---|---|
| Require login | Only a session that has passed license, login or register. |
| Open | Any 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.
| Endpoint | Limit | How it arrives |
|---|---|---|
| POST /api/client | 120 per minute per IP | Signed envelope with success false and rate_limited true |
| POST /api/v1/validate | 60 per minute per IP | HTTP 429 with {"detail": "..."} |
| POST /api/auth/login | 10 per minute per IP | HTTP 429 with {"detail": "..."} |
| POST /api/auth/register | 5 per hour per IP | HTTP 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-KeyFor the Discord bot and your own server-side scripts. Send your dashboard API key as the X-API-Key header.
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
| Status | Meaning |
|---|---|
| 401 | Missing or wrong credentials. |
| 403 | Authenticated, but not allowed. |
| 404 | Not found, or not owned by your account. |
| 409 | Already exists. |
| 422 | Input failed validation. |
| 429 | Rate 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.0There 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,<47C# / .NET 6+
dotnet add package NSec.Cryptography --version <the version you tested>C++17, Windows
vcpkg install libsodium nlohmann-jsonVerify 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.