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. 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
- 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 and create an app. It gets an Ed25519 key pair. Copy its app_key and pubkey_hex. The public key ships in your client.
2. Mint a license key
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://api.sentry.example/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 | 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
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. - 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, X-File-Sig (ed25519 over the file), and X-File-Version. |
POST /poll | session | Long-poll control channel (protocol v2). |
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 on auth. The response then includes a session token, and the client can hold a control channel with POST /poll: the server holds the request up to ~25 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 doesn't send proto:2 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 appends these fields after the v=1 core, in this fixed order:
v=3 is a superset of v=2: it also carries the session token and cver, so 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://api.sentry.example/api/v1/manage/apps/$APP_ID/keys \ -H 'authorization: Bearer sk_live_...' -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, set a Guild ID and Bot Token (stored encrypted). Auth is then denied unless the key's Discord user is currently a member of that server — checked live and cached ~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; a misconfigured token fails open so it can't lock out everyone.
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://your-host/api/v1/manage/apps/$APP_ID/keys \ -H 'authorization: Bearer sk_live_...' \ -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=<hmac> header (HMAC-SHA256 over 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 client authentication succeeds / fails |
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 banned hwid / ip / username is blocked |
file.download | a build is downloaded |
Every payload is { event, app_id, ts, data }. 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": "app_...", "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://your-host/discord/interactions, paste its token (we detect the app id + key), invite it, Register commands. | Full custom name and avatar. |
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.
C# / .NET
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);
C / C++
#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 */ }
Each SDK ships with a README covering the full API, the build and link setup, and the long-poll loop.
Enterprise & on-prem
The hosted service needs no infrastructure on your side. Self-managed and on-premise deployments are available on the Enterprise plan for compliance, data-residency, or air-gapped environments.
Enterprise also adds SSO, audit-log exports, an SLA, and priority support. App signing keys are encrypted at rest and never leave your tenant.
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.