License Verification Server licenseverificationserver.com
Sign In Get Started

API Reference

The License Verification Server API lets you activate, verify, deactivate, and manage software licenses programmatically. All tenant API calls are scoped to your account — you can only see your own license tenants, licenses, and customers.

Authentication

Different endpoint groups authenticate differently — choose the model that matches your integration:

Client-program endpoints (activate / verify / deactivate / heartbeat / login / transfer)

No Authorization header is used. Pass the credential as license_key in the JSON request body. The value may be a license key, a tenant API key, or an account API key — the server resolves whichever is presented.

POST /api/v1/activate Content-Type: application/json { "license_key": "XXXX-YYYY-ZZZZ", "product_id": "my-app", "machine_id": "device-fingerprint-hash" }

Admin API endpoints (/api/v1/admin/…)

Require an Authorization: Bearer header carrying the platform admin key (or a valid admin-role session token).

Authorization: Bearer YOUR_ADMIN_KEY

Tenant-scoped endpoints (per-user grants, email relay, product register)

Accept a tenant API key in an Authorization: Bearer header or in the request body as license_key. Generate tenant API keys in your Developer Portal → API Keys. Keep your keys secret — they grant access to your tenant data. Revoke any key that may have been exposed.

Authorization: Bearer YOUR_TENANT_API_KEY

Base URL

https://www.licenseverificationserver.com

All endpoints are served over HTTPS. HTTP requests are rejected.

Endpoints

GET /api/v1/pubkey No auth required

Returns the server's Ed25519 public key. Bundle this key with your client software to verify license tokens offline.

Response

{ "alg": "ed25519", "kid": "abc123", "public_key": "base64url-encoded-public-key" }
POST /api/v1/activate

Activate a license on a device. Returns a signed license token the client can cache and verify offline. Enforces seat limits — if the seat limit is reached, returns 409 Conflict. Supports Idempotency-Key — see Idempotency below. If the license's type declares a required activation field (e.g. Drupal Websites → site_url, Ubuntu Websites → machine_id), a new activation that omits it returns 400 Bad Request.

Request body

FieldTypeRequiredDescription
license_keystringrequiredThe license key, tenant API key, or account API key to activate
product_idstringrequiredYour product's unique ID
machine_idstringrequiredUnique device identifier (hash of hardware fingerprint, UUID, etc.)
machine_namestringoptionalHuman-readable device name for the admin dashboard
site_urlstringoptionalFull scheme+host of the licensed website/installation (e.g. https://example.com); stored per-activation and shown on the Drupal Sites page. Must start with http:// or https://.
customerobjectoptionalOptional customer intake object with any of: name, email, phone, company, address, city, region, country, postal_code, timezone, tags, notes, email_opt_in.
curl -X POST https://www.licenseverificationserver.com/api/v1/activate \ -H "Content-Type: application/json" \ -d '{ "license_key": "XXXX-YYYY-ZZZZ", "product_id": "my-app", "machine_id": "device-fingerprint-hash", "machine_name": "Work Laptop" }'
import requests resp = requests.post( "https://www.licenseverificationserver.com/api/v1/activate", json={ "license_key": "XXXX-YYYY-ZZZZ", "product_id": "my-app", "machine_id": "device-fingerprint-hash", "machine_name": "Work Laptop", }, ) data = resp.json() token = data["token"] # cache this locally
const res = await fetch( "https://www.licenseverificationserver.com/api/v1/activate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ license_key: "XXXX-YYYY-ZZZZ", product_id: "my-app", machine_id: "device-fingerprint-hash", machine_name: "Work Laptop", }), } ); const { token, license } = await res.json();

Response

{ "ok": true, "token": "...(signed license token)", "license": { "key": "XXXX-YYYY-ZZZZ", "product_id": "my-app", "tier": "standard", "seat_limit": 2, "seats_used": 1, "status": "active", "expires_at": "2027-01-01T00:00:00Z", "features": {} } }
POST /api/v1/verify

Verify a license and get a fresh signed token. Called periodically to refresh the cached token (e.g. every 7 days) and check for revocations or expirations. Requires an active activation on the device.

Request body

FieldTypeRequiredDescription
license_keystringrequired*Required unless a valid token is provided (key, tenant API key, or account API key)
machine_idstringrequired*Device identifier; can be extracted from a supplied token
product_idstringoptionalProduct ID; extracted from token if present
tokenstringoptionalExisting cached signed license token — license_key, machine_id, and product_id are extracted from it if present
curl -X POST https://www.licenseverificationserver.com/api/v1/verify \ -H "Content-Type: application/json" \ -d '{"license_key":"XXXX-YYYY-ZZZZ","machine_id":"device-hash"}'
POST /api/v1/deactivate

Deactivate a license on a specific device, freeing up a seat. Call this when the user uninstalls or explicitly deactivates your software.

curl -X POST https://www.licenseverificationserver.com/api/v1/deactivate \ -H "Content-Type: application/json" \ -d '{"license_key":"XXXX-YYYY-ZZZZ","machine_id":"device-hash"}'

Response

{"ok": true}
POST /api/v1/heartbeat

Touch the activation's last_seen_at timestamp. Send periodically (e.g. hourly) while the software is running to keep the activation record fresh. Returns 403 if the device is not activated or has been blocked.

curl -X POST https://www.licenseverificationserver.com/api/v1/heartbeat \ -H "Content-Type: application/json" \ -d '{"license_key":"XXXX-YYYY-ZZZZ","machine_id":"device-hash"}'
{"ok": true}
POST /api/v1/login/request-otp No auth required

Request a 6-digit OTP email verification code for client-program login. Always returns {"ok": true} regardless of whether the user exists, to prevent username enumeration. The code expires after 10 minutes and is single-use.

Request body

FieldTypeRequiredDescription
usernamestringrequiredUsername or email address
product_idstringoptionalProduct ID — scopes the OTP to a specific product
{"ok": true}
POST /api/v1/login No auth required

Authenticate an end user and mint a machine-bound signed session token, a signed license token, and an offline-login blob. The OTP gate is server-enforced: a non-admin user signing in from a new device must supply a valid otp_code when the account has an email address and email is configured. Call /api/v1/login/request-otp first to receive the code. Known devices (already activated on the license) and admin users are exempt from OTP.

Request body

FieldTypeRequiredDescription
usernamestringrequiredUsername or email address
passwordstringrequiredAccount password
machine_idstringrequiredUnique device identifier for machine-binding
machine_namestringoptionalHuman-readable device name
product_idstringoptionalProduct to log in to; used to pick the correct license when the user has several
license_keystringoptionalExplicit license key hint (overrides auto-pick)
otp_codestringconditional6-digit OTP code required for new-device logins when email is configured

Response

{ "ok": true, "token": "...(signed license token)", "session_token": "...(signed session token)", "offline_login": "...(machine-bound offline-login blob)", "license": { "key": "...", "product_id": "...", "tier": "...", ... } }

Returns 401 when OTP is required but not supplied (detail: "verification code required"), or when an OTP code is supplied but invalid/expired. Returns 403 when the account is disabled. Rate-limited to 5 attempts per 15 minutes per IP.

POST /api/v1/session/info No auth required

Verify a signed session token and return its claims without refreshing the session or interacting with any seat. Intended for first-party services (e.g. a website that receives a token from a client program) that need to identify the token owner.

Request body

FieldTypeRequiredDescription
session_tokenstringrequiredThe signed session token to verify

Response

{ "ok": true, "username": "alice", "is_admin": false, "tier": "standard", "product_id": "my-app", "expires_at": "2026-07-01T12:00:00Z" }
POST /api/v1/transfer

Move a license to a new device, freeing all other active seats. Requires a valid signed session token for the owning account. Enforces a per-license rolling transfer cooldown window and a self-service reactivation budget.

Request body

FieldTypeRequiredDescription
session_tokenstringrequiredValid signed session token for the owning account
license_keystringrequiredLicense key to transfer
machine_idstringrequiredThe new machine taking the seat
machine_namestringoptionalHuman-readable name for the new device

Response

{ "ok": true, "token": "...(signed license token)", "session_token": "...(refreshed session token)", "license": { "key": "...", ... } }

Returns 403 if the session does not own the license. Returns 409 if the self-service reactivation budget is exhausted. Returns 429 if the rolling transfer limit is reached.

POST /api/v1/license/users/authorize

Grant an individual end-user a seat on a license. Returns a 24-hour Ed25519-signed grant token the client should cache for offline use. If the user is already granted, the call is idempotent and returns the same status: "granted" response. If the seat cap is reached, returns status: "rejected" with HTTP 200 — rejection never blocks login; only licensed features are withheld. Auth: pass the license key (or tenant API key) as license_key in the request body.

Request body

FieldTypeRequiredDescription
license_keystringrequiredLicense key or tenant API key
external_user_idstringrequiredOpaque user identifier from the client system (e.g. Drupal uid)
kindstringoptional"user" (default) or "admin" — determines which seat pool to count against
displaystringoptionalHuman-readable label (email/username) shown in the admin dashboard

Response (granted)

{ "status": "granted", "grant_token": "...(24-hour signed grant token)", "expires_at": "2026-06-13T12:00:00Z", "used": 3, "limit": 10 }

Response (cap reached)

{ "status": "rejected", "reason": "limit_reached", "used": 10, "limit": 10, "kind": "user" }
POST /api/v1/license/users/revoke

Free an end-user's seat by revoking their grant. The seat is immediately available for another user. Auth: pass the license key (or tenant API key) as license_key in the request body.

Request body

FieldTypeRequiredDescription
license_keystringrequiredLicense key or tenant API key
external_user_idstringrequiredThe user ID to revoke

Response

{"status": "revoked"} // or {"status": "not_found"}

Error Codes

HTTP StatusMeaning
400Bad request — malformed JSON or missing required fields
401Unauthorized — invalid or expired OTP code, or OTP required but not supplied (login flow only)
403Forbidden — unknown license key, license revoked/suspended/expired, device blocked, or session does not own the license
409Conflict — seat limit reached, or reactivation budget exhausted
422Unprocessable entity — request body failed Pydantic validation (missing required field or wrong type)
429Too many requests — login rate limit, transfer limit, or per-endpoint rate limit exceeded

All error responses carry a detail field: {"detail": "description"}

Rate Limits

There is no fixed per-tenant API rate limit. Individual endpoints apply per-IP limits using an in-process sliding-window counter:

EndpointLimit
POST /api/v1/login, /api/v1/check-account5 attempts per 15 minutes per IP
POST /api/v1/deactivate30 calls per 60 seconds per IP
POST /api/v1/offline/activate10 calls per 60 seconds per IP
All other /api/v1/ endpointsNo fixed limit (fair-use)

When a rate limit is exceeded the server returns 429 Too Many Requests with a detail message. A Retry-After header is not currently set; client programs should back off with exponential jitter.

Idempotency

POST /api/v1/activate and POST /api/v1/payments/intent support idempotent retries. Include an Idempotency-Key header with a unique string (UUID recommended). If the server has already processed a request with that key, it replays the original response instead of re-executing the operation — preventing double seat-claims or double payment intents on network retries.

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

A replayed response includes the header Idempotent-Replay: true. Keys are scoped per endpoint (an activate key does not collide with a payments/intent key). The cache is held in-process; a server restart may not replay. Use a fresh key for each new logical operation.

Questions? Go to your portal or view pricing.