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
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 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

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

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 cookiethe 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

Client │ POST /api/v1/auth { app_key, key, hwid, nonce } ▼ Public API │ check license (valid, not expired, not disabled) │ check HWID / seat limit │ check bans (hwid, ip) │ sign the response with the app's Ed25519 private key ▼ Client verify the signature, then read 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.
  • 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, X-File-Sig (ed25519 over the file), and X-File-Version.
POST /pollsessionLong-poll control channel (protocol v2).
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 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.

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 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.

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://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.

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

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=<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.

EventFires when
auth.success / auth.faila client authentication succeeds / fails
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 banned hwid / ip / username is blocked
file.downloada 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.

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

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://your-host/discord/interactions, paste its token (we detect the app id + key), invite it, Register commands.Full custom name and avatar.

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.

C# / .NET

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);

C / C++

main.c · libcurl + libsodium
#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.

Need on-prem, SSO or a security review? Get in touch 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