Licensing API
Plugin-side endpoints for license activation, heartbeat, validation, and deactivation. Called from your Minecraft or Rust plugin at runtime.
Base URL & Auth
https://pulseplugin.net/api/v1/licenseAll endpoints accept and return JSON. No API key or auth header required — the license key itself is the credential.
PULSE-XXXX-XXXX-XXXX-XXXX. Each buyer has a unique key per product.Activate
/activateRegister a server with a license key. Call this on plugin startup.
Request Body
keystringrequiredThe license key (PULSE-XXXX-XXXX-XXXX-XXXX)server_idstringrequiredUnique server identifier — persist this UUID across restartsserver_labelstringFriendly label shown in the buyer dashboardplugin_versionstringCurrent plugin version — used for min-version enforcementserver_fingerprintstringSHA256 of machine GUID + primary MAC + hostname — used for clone detectionnoncestringRandom 16+ char hex string — echoed in JWT and response for replay protection (recommended)// 200 OK — activation successful
{
"success": true,
"token": "eyJ...",
"expires_in_hours": 48,
"offline_grace_hours": 24,
"nonce_echo": "a3f8b2c1d4e5f6a7b8c9d0e1",
"license": {
"key": "PULSE-XXXX-XXXX-XXXX-XXXX",
"product_name": "MyPlugin",
"buyer_username": "cooldev",
"max_activations": 1,
"active_activations": 1
}
}
// 403 — max activations reached
{ "success": false, "error": "max_activations_reached", "current": 1, "max": 1 }
// 403 — plugin version too old
{ "success": false, "error": "version_deprecated", "message": "Version 1.0.0 is no longer supported. Please update to 2.1.0 or later.", "min_version": "2.1.0" }
// 403 — revoked or suspended
{ "success": false, "error": "license_revoked" }
// 404 — key not found
{ "success": false, "error": "invalid_key" }
// 429 — too many activation attempts
{ "success": false, "error": "rate_limited", "message": "Too many activation attempts. Please wait before trying again.", "retry_after": 240 }Heartbeat
/heartbeatUpdate last-seen timestamp and receive a fresh JWT. Use next_heartbeat_recommended to schedule the next call.
Request Body
keystringrequiredThe license keyserver_idstringrequiredServer identifier (must match the activation)plugin_versionstringCurrent plugin versionnoncestringRandom 16+ char hex string — echoed in JWT to prevent replay attacks (recommended)// 200 OK
{
"valid": true,
"token": "eyJ...",
"expires_in_hours": 48,
"next_heartbeat_recommended": 1800,
"nonce_echo": "a3f8b2c1d4e5f6a7"
}
// 403 — not activated or revoked
{ "valid": false, "error": "not_activated" }next_heartbeat_recommended (in seconds) to schedule your next heartbeat rather than hardcoding 30 minutes. The interval adapts to server load — it may increase to up to 3600s during high traffic.nonce in each heartbeat request. The server echoes it inside the signed JWT — verify that the nonce claim in the decoded token matches what you sent. This prevents a captured response from being replayed.Validate
/validateLightweight status check. Does not update the last-seen timestamp.
Request Body
keystringrequiredThe license keyserver_idstringrequiredServer identifier// 200 OK
{
"valid": true,
"status": "active",
"offline_grace_hours": 24
}
// status values: "active" | "revoked" | "suspended" | "not_activated"Deactivate
/deactivateFree an activation slot. Use when migrating the plugin to a different server.
Request Body
keystringrequiredThe license keyserver_idstringrequiredServer identifier to deactivatetokenstringCurrent cached JWT — prevents unauthorized deactivation if key is stolen (recommended)// 200 OK
{ "success": true }
// 403 — token provided but doesn't match
{ "success": false, "error": "invalid_token", "message": "The provided token does not match this license activation." }
// 404 — activation not found
{ "success": false, "error": "not_found" }tokenfield. An attacker who obtains your license key from a config file cannot deactivate your server if they don't also have the JWT — which should be stored separately, ideally encrypted.Public Key
/public-keyReturns all currently valid Ed25519 public keys for offline JWT verification.
// 200 OK
{
"keys": [
{
"kid": "7f413c835f2cc613",
"algorithm": "EdDSA",
"public_key": "Sx4jid6K34TpfGOEpocqCtpoXDISFeNv1ll0lu5QXoU=",
"valid_until": null
}
]
}kid header in the JWT to select the correct verification key. Fetch and cache this list on plugin startup.Offline Mode
After a successful activation or heartbeat, your plugin receives a JWT token. Cache this to disk. If the network is unavailable on the next startup:
- Decode the JWT and check the expiry (
expclaim) - If within the token TTL + offline grace period, allow the plugin to continue
- If outside the grace period, block with a message asking the user to check their internet connection
license_revoked or invalid_key. Network errors should fall through to the offline grace period silently.JWT Claims
Every token returned by /activate and /heartbeat is a signed Ed25519 JWT. Decode it without a network call to verify it offline.
Header
| Field | Value | Description |
|---|---|---|
alg | EdDSA | Ed25519 signature algorithm |
typ | JWT | Token type |
kid | string | Key ID — select the matching public key from /public-key |
Payload
| Claim | Type | Description |
|---|---|---|
iss | string | Issuer — always "pulseplugin.net". Reject tokens with any other issuer. |
sub | string | Subject — the license key (e.g. PULSE-XXXX-XXXX-XXXX-XXXX) |
aud | string | Audience — the product GUID. Reject tokens where aud ≠ your product GUID. |
iat | number | Issued at (Unix timestamp) |
exp | number | Expires at (Unix timestamp) — always iat + 172800 (48 hours) |
jti | string | Unique token ID (UUID) — for revocation tracking |
key | string | License key (same as sub — kept for clarity) |
product_id | string | Product GUID (same as aud — kept for clarity) |
srv | string | Server identifier (same as server_id) |
server_id | string | Server identifier (kept for backward compat) |
buyer_id | string | PulsePlugin user ID of the license owner |
grace_hours | number | Per-product offline grace period in hours |
fp | string? | SHA256 server fingerprint (present if sent on activate) |
nonce | string? | Echoed nonce (present if sent on activate or heartbeat) |
plugin_version | string? | Plugin version (present if sent on activate or heartbeat) |
Example Payload
{
"iss": "pulseplugin.net",
"sub": "PULSE-Q3BG-0HSL-SU7V-6UH3",
"aud": "342c3d8f-4821-4026-a580-ea38de9fe4ff",
"iat": 1777636530,
"exp": 1777809330,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"key": "PULSE-Q3BG-0HSL-SU7V-6UH3",
"product_id": "342c3d8f-4821-4026-a580-ea38de9fe4ff",
"srv": "my-server-001",
"server_id": "my-server-001",
"buyer_id": "180fce47-2e8b-4264-b2e3-aaa6818e2482",
"grace_hours": 24,
"fp": "a3f8b2c1d4e5f6a7...",
"nonce": "deadbeef12345678",
"plugin_version": "2.1.0"
}Plugin Verification Checklist
When verifying a token offline, your plugin MUST check all of these:
- Signature is valid using the public key matching
kid iss==="pulseplugin.net"— reject if wrong issueraud=== your product GUID — reject if wrong productexp> current Unix time — reject if expiredexp + grace_hours × 3600> current time — offline grace check- If
fpis present: equals locally computed fingerprint — reject if machine mismatch - If you sent a nonce:
nonceclaim in the JWT payload equals the nonce you sent — verify from the signed token, not fromnonce_echoin the response body (which is unsigned and can be forged)
Key Selection
The kid field in the JWT header tells you which public key to use. Fetch /public-key on startup, cache all returned keys, and select by kid:
// Pseudocode
Map<String, PublicKey> keyring = fetchPublicKeys(); // from /public-key
String kid = decodeHeader(token).kid;
PublicKey key = keyring.get(kid);
boolean valid = verifySignature(token, key);Server Fingerprint
To protect against server cloning (copying the entire server folder to a second machine), include a server_fingerprint in your activate request. Compute it as:
SHA256(machine_guid + "|" + primary_mac_address + "|" + hostname)If the same server_id activates with a different fingerprint, the platform logs a mismatch. After repeated mismatches, the seller is notified.
private String computeFingerprint() {
try {
String machineGuid = getMachineGUID();
String mac = getPrimaryMacAddress();
String hostname = InetAddress.getLocalHost().getHostName();
String raw = machineGuid + "|" + mac + "|" + hostname;
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (Exception e) {
return null; // optional — don't block on failure
}
}Error Codes
| Code | HTTP | Description |
|---|---|---|
invalid_key | 404 | License key does not exist |
license_revoked | 403 | License has been revoked |
license_suspended | 403 | License is temporarily suspended |
license_expired | 403 | Subscription expired or license past expiry |
max_activations_reached | 403 | All activation slots are in use |
not_activated | 403 | Server not activated for this license |
rate_limited | 429 | Too many requests — back off and retry using retry_after |
version_deprecated | 403 | Plugin version below seller-set minimum |
invalid_token | 403 | JWT token mismatch on deactivate |