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.
Admin API endpoints (/api/v1/admin/…)
Require an Authorization: Bearer header carrying
the platform admin key (or a valid admin-role session token).
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.
Base URL
All endpoints are served over HTTPS. HTTP requests are rejected.
Endpoints
Returns the server's Ed25519 public key. Bundle this key with your client software to verify license tokens offline.
Response
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
| Field | Type | Required | Description |
|---|---|---|---|
| license_key | string | required | The license key, tenant API key, or account API key to activate |
| product_id | string | required | Your product's unique ID |
| machine_id | string | required | Unique device identifier (hash of hardware fingerprint, UUID, etc.) |
| machine_name | string | optional | Human-readable device name for the admin dashboard |
| site_url | string | optional | Full 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://. |
| customer | object | optional | Optional customer intake object with any of: name, email, phone, company, address, city, region, country, postal_code, timezone, tags, notes, email_opt_in. |
Response
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
| Field | Type | Required | Description |
|---|---|---|---|
| license_key | string | required* | Required unless a valid token is provided (key, tenant API key, or account API key) |
| machine_id | string | required* | Device identifier; can be extracted from a supplied token |
| product_id | string | optional | Product ID; extracted from token if present |
| token | string | optional | Existing cached signed license token — license_key, machine_id, and product_id are extracted from it if present |
Deactivate a license on a specific device, freeing up a seat. Call this when the user uninstalls or explicitly deactivates your software.
Response
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| username | string | required | Username or email address |
| product_id | string | optional | Product ID — scopes the OTP to a specific product |
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
| Field | Type | Required | Description |
|---|---|---|---|
| username | string | required | Username or email address |
| password | string | required | Account password |
| machine_id | string | required | Unique device identifier for machine-binding |
| machine_name | string | optional | Human-readable device name |
| product_id | string | optional | Product to log in to; used to pick the correct license when the user has several |
| license_key | string | optional | Explicit license key hint (overrides auto-pick) |
| otp_code | string | conditional | 6-digit OTP code required for new-device logins when email is configured |
Response
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| session_token | string | required | The signed session token to verify |
Response
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
| Field | Type | Required | Description |
|---|---|---|---|
| session_token | string | required | Valid signed session token for the owning account |
| license_key | string | required | License key to transfer |
| machine_id | string | required | The new machine taking the seat |
| machine_name | string | optional | Human-readable name for the new device |
Response
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.
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
| Field | Type | Required | Description |
|---|---|---|---|
| license_key | string | required | License key or tenant API key |
| external_user_id | string | required | Opaque user identifier from the client system (e.g. Drupal uid) |
| kind | string | optional | "user" (default) or "admin" — determines which seat pool to count against |
| display | string | optional | Human-readable label (email/username) shown in the admin dashboard |
Response (granted)
Response (cap reached)
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
| Field | Type | Required | Description |
|---|---|---|---|
| license_key | string | required | License key or tenant API key |
| external_user_id | string | required | The user ID to revoke |
Response
Error Codes
| HTTP Status | Meaning |
|---|---|
| 400 | Bad request — malformed JSON or missing required fields |
| 401 | Unauthorized — invalid or expired OTP code, or OTP required but not supplied (login flow only) |
| 403 | Forbidden — unknown license key, license revoked/suspended/expired, device blocked, or session does not own the license |
| 409 | Conflict — seat limit reached, or reactivation budget exhausted |
| 422 | Unprocessable entity — request body failed Pydantic validation (missing required field or wrong type) |
| 429 | Too 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:
| Endpoint | Limit |
|---|---|
| POST /api/v1/login, /api/v1/check-account | 5 attempts per 15 minutes per IP |
| POST /api/v1/deactivate | 30 calls per 60 seconds per IP |
| POST /api/v1/offline/activate | 10 calls per 60 seconds per IP |
| All other /api/v1/ endpoints | No 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.
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.