Documentation
Sentry handles licensing, authentication, software delivery, and user management for desktop applications. This guide covers the APIs, the signed authentication response, and the SDKs.
Introduction
Applications authenticate against the public API with an app key plus either a license key or user credentials. Each application has its own Ed25519 key pair. Successful auth responses are signed with the private key and verified in your client against the embedded public key, so a client can tell a real response from a forged one.
Licenses, builds, users, bans and webhooks are managed from the dashboard or the management API.
The platform includes:
- Tenant dashboard
- Public client API: what your shipped software calls
- Management API: server-to-server automation
- Webhooks
- C/C++ and C#/.NET SDKs, in the integration kit
- Ed25519-signed authentication responses
Quickstart
These steps use the hosted API. There is nothing to install.
1. Create an app
Sign in to the dashboard with Discord and create an app. It gets an Ed25519 key pair. On the app's Settings page, copy its app_key (General) and its public key (Public Key, 64 hex characters). The public key ships in your client.
2. Mint a license key
On the Licenses page, create keys with seats, an expiry and tags, one at a time or in bulk from a template.
3. Authenticate a key
curl -s https://sentryauth.xyz/api/v1/auth \ -H 'content-type: application/json' \ -d '{"app_key":"app_...","key":"KEY-XXXX-YYYY","hwid":"machine-id","nonce":"a1b2"}'
4. Verify the response
Call auth from an SDK. It checks sig against the public key before reading any field, so you don't touch the signature yourself.
sd from the JSON fields. Verify the exact bytes the server returned, then read the fields out of them.Architecture
There are three APIs, each with its own authentication method:
| API | Path | Auth | Called by |
|---|---|---|---|
| Public client | /api/v1/* | app_key + license or user credentials | your shipped software |
| Dashboard | /account/*, /dashboard/api/* | session cookie from Discord sign-in | the tenant dashboard |
| Management | /api/v1/manage/* | Authorization: Bearer | your backend / automation |
The app key identifies the application on every public call. Application-scoped queries are constrained by the owning tenant, so one tenant can't read or modify another's data.
Authentication flow
- ClientPOST
/api/v1/authwith{ app_key, key, hwid, nonce } - Public APIChecks the license (valid, not expired, not disabled), the HWID and seat limit, and bans (hwid, ip), then signs the response with the app’s Ed25519 private key.
- ClientVerifies the signature, then reads the fields.
Authentication response
A successful authentication returns two fields:
| Field | Description |
|---|---|
sd | The signed payload: key=value pairs joined with |. |
sig | Ed25519 signature over the bytes of sd, base64-encoded. |
v=1|ts=..|nonce=..|hwid=..|ok=1|username=..|name=..|expires=..|latest_version=..
Verify in this order and stop on the first failure:
| Check | Why |
|---|---|
Verify sig over the exact bytes of sd | Proves the response came from the app's private key, not a spoofed or proxied server. |
Check ok=1 | The request was allowed. |
Check nonce matches the one you sent | Confirms the response belongs to this request and blocks replayed responses. |
Check hwid matches this machine | Confirms the response was issued for this device. |
Check ts is recent | Rejects a stale response captured from an earlier session. |
Read name, expires and latest_version only after every check passes.
Developer notes
sdis UTF-8 and uses|as the field delimiter.- Field order is fixed. A new field bumps the
v=version; verify against the version you built against. vis 1, 2 or 3 and follows theprotoyour client sends:proto: 2gives v=2,proto: 3gives v=3, anything else v=1. v=2 and v=3 keep the v=1 fields above in the same order and append more (see sessions and v=3).- Ignore fields you don't recognize.
- Reject a
tsolder than five minutes. - Verify the signature before parsing anything out of
sd.
Public client API
Base path /api/v1. Select the app with app_key (JSON body on POST, query string on GET). Signed endpoints require a hwid and accept an optional nonce.
| Endpoint | Body | Purpose |
|---|---|---|
POST /auth | key, hwid, nonce? | Validate a bare license key. Signed response. |
POST /register | username, password, key, hwid | Create an account by redeeming a key. |
POST /login | username, password, hwid | Authenticate an existing account. |
POST /file/latest | key or username,password | Streams the latest build. Verify it via the response headers X-File-SHA256 (hex SHA-256 of the file), X-File-Sig (base64 ed25519 over the raw 32-byte SHA-256 digest, not over the file bytes), and X-File-Version. |
POST /poll | session | Long-poll control channel (protocol v2 or v3). |
GET /pubkey | query | The app's ed25519 public key (bootstrapping). |
GET /version | query | The advertised latest version. |
Seat enforcement is automatic: a key admits an hwid that is already a device, or while the key has fewer active devices than its seats; otherwise it is denied with seat_limit_reached.
Sessions & long-poll (protocol v2)
Send proto: 2 (or proto: 3) on auth. The signed response then carries a session token and cver after latest_version (v=2|ts|nonce|hwid|ok|username|name|expires|latest_version|session|cver), and the client can hold a control channel with POST /poll: the server holds the request up to 10 seconds and returns as soon as a directive is queued for that session, otherwise it returns idle.
| Directive | Meaning |
|---|---|
revoked | Session or device revoked. Stop the client. |
kill | App paused (kill switch). Stop the client. |
update_required | Client is below the enforced minimum version. |
idle | Window elapsed with nothing queued. Reconnect. |
A client that sends neither proto: 2 nor proto: 3 gets the v=1 response unchanged.
Discord identity & gating (v=3)
Bind a Discord user to a key at mint time, and it is echoed into the signed response so your client can show who is signed in (name + avatar). Opt in per request with proto: 3; v=1 and v=2 clients are unaffected.
v=3 is a superset of v=2: after the v=1 fields it carries the session token and cver, then the five fields below, in this fixed order. A v=3 client gets a session and the Discord fields in one response.
v=3|ts=..|nonce=..|hwid=..|ok=1|username=..|name=..|expires=..|latest_version=.. |session=..|cver=..|discord_id=..|discord_username=..|discord_avatar=..|text_hash=..|force_min_version=..
| Field | Meaning |
|---|---|
discord_id | Discord user id bound to the key (empty if none). |
discord_username | Display name to show in your UI. |
discord_avatar | Avatar hash. Build the URL yourself: cdn.discordapp.com/avatars/<id>/<hash>.png. |
text_hash | SHA-256 of the latest build's PE .text section, or empty. Compare against your own code section to detect tampering. |
force_min_version | Minimum client version, or empty. Clients below it should force-update. |
Bind a Discord user at mint
Pass the identity when you create a key from your bot or backend:
curl -X POST https://sentryauth.xyz/api/v1/manage/apps/$APP_ID/keys \ -H 'authorization: Bearer sk_...' -H 'content-type: application/json' \ -d '{"count":1,"discord_id":"123...","discord_username":"alice","discord_avatar":"a1b2c3"}'
Re-link after mint with POST /api/v1/manage/apps/:appId/keys/discord — body { key, discord_id, discord_username, discord_avatar }.
Guild membership gate
In Settings → Discord, click Connect Discord Server and leave Require membership on. Sentry's own bot does the check, so no bot token is needed. Auth is then denied unless the key's Discord user is currently a member of that server, checked live and cached for about 5 minutes. A user who leaves or is banned is locked out even before their key expires. Deny reasons: discord_left_guild (not a member) and discord_required (key has no linked Discord). Only a definitive "not a member" denies; if Discord can't be reached the check fails open, so it can't lock out everyone. Turn Require membership off to use the server only for the Discord bot.
Build integrity & version floor
When you upload a PE build, Sentry hashes its .text section and signs it in as text_hash. Non-PE uploads leave it empty. force_min_version comes from the same Settings page.
REST admin API
Base path /api/v1/manage, authenticated with Authorization: Bearer <token>. Create tokens in the dashboard with per-resource scopes and an optional expiry. An empty scope set means full access.
curl -X POST https://sentryauth.xyz/api/v1/manage/apps/$APP_ID/keys \ -H 'authorization: Bearer sk_...' \ -H 'content-type: application/json' \ -d '{"count":1,"expires_days":365}'
Scopes are drawn from keys, users, files and bans, each :read or :write. A route rejects an out-of-scope token with insufficient_scope; expired tokens are rejected at auth.
Optional fields when creating keys: discord_id / discord_username / discord_avatar bind a Discord identity that is echoed into the signed response (see v=3), and metadata (any JSON object) is stored on the key and passed straight through to the key.create webhook. Re-link a key's Discord later with POST /api/v1/manage/apps/:appId/keys/discord.
Bulk import (migration)
POST /api/v1/manage/apps/:appId/keys/import loads existing keys from another system, preserving the original key string plus HWID, expiry, Discord identity and disabled state. It does not fire key.create webhooks (it isn't a sale). Up to 1000 keys per call.
{
"on_conflict": "skip" | "update",
"keys": [
{ "key": "XXXX-YYYY", "name": "plan", "hwid": "<bound machine or null>",
"expires_at": 1732592000, "duration_days": null, "disabled": false,
"discord_id": "123...", "discord_username": "..", "discord_avatar": "..",
"metadata": { }, "created_at": 1700000000 }
]
}expires_at / created_at accept unix seconds or ISO. Use expires_at for an absolute expiry, or duration_days for a clock that starts on first use. Returns { imported, updated, skipped, total, errors }.
Webhooks
Register HTTPS endpoints to receive events. Deliveries are POSTed as JSON, signed with a per-webhook secret in the x-sentry-signature: sha256=<hex> header (hex HMAC-SHA256 of the raw body). Targets are validated against SSRF at creation and at delivery; private, loopback, link-local and metadata addresses are blocked, and redirects are not followed.
| Event | Fires when |
|---|---|
auth.success / auth.fail | a license-key auth (/auth) succeeds / is tried with an unknown key |
user.register / user.login | an end-user account registers / logs in |
key.create | a key is minted (dashboard, REST, or the bot) |
key.redeem | a key is first claimed by a user |
ban.hit | a license-key auth (/auth) is blocked by an hwid or ip ban |
file.download | a build is downloaded |
Other auth failures, and bans hit on /login, /register or /file/latest, are logged but don't fire a webhook.
Every payload is { event, app_id, ts, data }, where app_id is the app's UUID (not its app_ key) and ts is unix seconds. key.create is the integration hook for billing or affiliate systems: its data carries who minted it, the target Discord user, and any metadata you attached at mint.
{
"event": "key.create", "app_id": "3f1c9a52-...", "ts": 1730000000,
"data": {
"key": "XXXX-YYYY-ZZZZ", "name": "Customer",
"actor_discord_id": "918...", "target_discord_id": "123...",
"expires": 1732592000,
"metadata": { "price_cents": 2500, "referrer_code": "kodi-vip" }
}
}Discord bot
Sentry includes a Discord bot so your staff can mint and manage keys from inside Discord. Configure it per app under Settings → Bot. It runs on a single HTTP interactions endpoint, so there is no bot process to host and it costs nothing to add. Two modes:
| Mode | Setup | Branding |
|---|---|---|
| Shared | Connect your server in Settings → Discord, enable, Register commands. | Custom name per server (nickname); avatar is Sentry's. |
| Own bot | Create a Discord app, set its Interactions Endpoint URL to https://sentryauth.xyz/discord/interactions, paste its token (we detect the app id + key), invite it, Register commands. | Full custom name and avatar. |
One bot can serve several of your products in the same server. Its commands then get a required product option, and each product keeps its own roles and DM settings.
Commands
| Command | Does |
|---|---|
/generate user length [name] [trial] [metadata] | Mint a key bound to a member. Assigns the customer role, DMs the key, and notifies the owner. |
/key info | reset-hwid | disable | enable | extend | delete | Manage a key by its string or by @user. |
/stats | Total / active / HWID-bound keys and 24h auth count. |
Behaviour is configured in the dashboard: role gates (a management role for everything, a keygen role for /generate only), an auto-assigned customer role (skippable for trials), a templated customer DM, and an owner-notify DM with an optional Mark as Paid button. Length accepts 7d, 30d, 1m, 1y, lifetime, or free-text like 90d. Every mint fires the key.create webhook, so your own systems can record the sale.
SDKs
The SDKs do the signature verification, manage the session, and handle the long-poll directives. They support protocol v3, so the verified result carries the Discord identity, the build .text hash, and the version floor. Available for C/C++ and C#/.NET: download the integration kit.
using Sentry; using var client = new SentryClient(baseUrl, appKey, publicKeyHex, useProtocolV3: true, device: new SentryDeviceInfo { Version = "1.4.70", Os = "Windows 11" }); var r = await client.AuthAsync("KEY-XXXX-YYYY"); if (!r.Ok) return; // r.Reason explains why // v3: r.DiscordUsername, r.TextHash, r.ForceMinVersion — all verified // live control channel (revoke / kill / update) await client.StartPollingAsync(evt => { if (evt.Directive == SentryDirective.Revoked) Environment.Exit(0); }, ct);
#include "sentry_auth.h" sentry_config cfg = {0}; cfg.base_url = base_url; cfg.app_key = app_key; cfg.pubkey_hex = pub_hex; cfg.use_proto3 = 1; sentry_client *c = sentry_client_new(&cfg, NULL); sentry_auth_result r; if (sentry_auth(c, "KEY-XXXX-YYYY", &r) == SENTRY_OK) { /* r.name, r.discord_username, r.text_hash — verified, safe to trust */ }
Both SDKs ship in the integration kit, each with a README covering the full API, the build and link setup, and the long-poll loop. The kit also has the full API reference.
Enterprise & on-prem
The hosted service needs no infrastructure on your side. Self-hosted and dedicated deployments are available on the Enterprise plan: Sentry runs as a Docker image or a single Bun binary against your own Postgres.
Enterprise also adds higher limits and priority support. App signing keys are encrypted at rest; only the public key is ever exposed.
Security posture
How the platform protects tenants and clients:
- Signed-string field injection: values reflected into
sdare allowlisted at input and stripped of delimiters. - One key, one account: redemption is an atomic conditional update backed by a uniqueness constraint.
- HWID / seat locks: enforced on every auth path.
- Webhook SSRF: private/loopback/metadata targets blocked at creation and delivery.
- CSRF: Origin/Referer checked on cookie-authenticated mutations, with SameSite cookies.
- Encrypted key vault: per-app private seeds are AES-256-GCM encrypted; only public keys are exposed.
- Role-gated dashboard: read-only members can view but not mutate; team/token/audit routes require admin.