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
New to the API? Start with the Quickstart: create an app, mint a key, and authenticate it.

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

POST /api/v1/auth
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.

Common mistakeDon't rebuild 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:

APIPathAuthCalled by
Public client/api/v1/*app_key + license or user credentialsyour shipped software
Dashboard/account/*, /dashboard/api/*session cookie from Discord sign-inthe tenant dashboard
Management/api/v1/manage/*Authorization: Beareryour 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

  1. ClientPOST /api/v1/auth with { app_key, key, hwid, nonce }
  2. 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.
  3. ClientVerifies the signature, then reads the fields.

Authentication response

A successful authentication returns two fields:

FieldDescription
sdThe signed payload: key=value pairs joined with |.
sigEd25519 signature over the bytes of sd, base64-encoded.
sd
v=1|ts=..|nonce=..|hwid=..|ok=1|username=..|name=..|expires=..|latest_version=..

Verify in this order and stop on the first failure:

CheckWhy
Verify sig over the exact bytes of sdProves the response came from the app's private key, not a spoofed or proxied server.
Check ok=1The request was allowed.
Check nonce matches the one you sentConfirms the response belongs to this request and blocks replayed responses.
Check hwid matches this machineConfirms the response was issued for this device.
Check ts is recentRejects a stale response captured from an earlier session.

Read name, expires and latest_version only after every check passes.

Rotating keysIf you rotate an application's signing key, existing clients can no longer verify new responses. Ship an update carrying the new public key before you rotate a production key.

Developer notes

  • sd is 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.
  • v is 1, 2 or 3 and follows the proto your client sends: proto: 2 gives v=2, proto: 3 gives 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 ts older 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.

EndpointBodyPurpose
POST /authkey, hwid, nonce?Validate a bare license key. Signed response.
POST /registerusername, password, key, hwidCreate an account by redeeming a key.
POST /loginusername, password, hwidAuthenticate an existing account.
POST /file/latestkey or username,passwordStreams 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 /pollsessionLong-poll control channel (protocol v2 or v3).
GET /pubkeyqueryThe app's ed25519 public key (bootstrapping).
GET /versionqueryThe 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.

DirectiveMeaning
revokedSession or device revoked. Stop the client.
killApp paused (kill switch). Stop the client.
update_requiredClient is below the enforced minimum version.
idleWindow 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.

sd (v=3)
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=..
FieldMeaning
discord_idDiscord user id bound to the key (empty if none).
discord_usernameDisplay name to show in your UI.
discord_avatarAvatar hash. Build the URL yourself: cdn.discordapp.com/avatars/<id>/<hash>.png.
text_hashSHA-256 of the latest build's PE .text section, or empty. Compare against your own code section to detect tampering.
force_min_versionMinimum 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:

POST /api/v1/manage/apps/:appId/keys
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.

issue a key from your own systems
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.

POST /api/v1/manage/apps/:appId/keys/import
{
  "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.

EventFires when
auth.success / auth.faila license-key auth (/auth) succeeds / is tried with an unknown key
user.register / user.loginan end-user account registers / logs in
key.createa key is minted (dashboard, REST, or the bot)
key.redeema key is first claimed by a user
ban.hita license-key auth (/auth) is blocked by an hwid or ip ban
file.downloada 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.

key.create
{
  "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:

ModeSetupBranding
SharedConnect your server in Settings → Discord, enable, Register commands.Custom name per server (nickname); avatar is Sentry's.
Own botCreate 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

CommandDoes
/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 | deleteManage a key by its string or by @user.
/statsTotal / 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.

Program.cs
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);

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.

Need self-hosting or higher limits? Request an upgrade from the dashboard and we’ll scope an Enterprise plan to your requirements.

Security posture

How the platform protects tenants and clients:

  • Signed-string field injection: values reflected into sd are 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.
Sentry · licensing & product authentication. Home  ·  Dashboard