DocsAPILicense Verification API

License Verification API

Technical reference for the license verification endpoint, token format, Ed25519 signatures, and offline verification.

Overview

The license verification API allows the Nareli desktop app (and any authorized client) to verify a license key and receive a signed token that can be validated offline. The API supports both GET and POST methods and returns a JSON response indicating whether the license is valid, along with a cryptographically signed token containing the full license payload.

Endpoint

The verification endpoint accepts both GET and POST requests. For POST requests, send a JSON body with the license key and an optional user ID. For GET requests, pass the same parameters as query strings. POST is recommended for security as it avoids logging the license key in server access logs.

POST /api/verify/license
Content-Type: application/json

{
  "license": "NRLI-A3F1-8B2C-D4E7-9F06",
  "userId": "optional-user-id"
}

Request Parameters

The request accepts two parameters. The "license" field (required) is your license key in the format NRLI-XXXX-XXXX-XXXX-XXXX. The "userId" field (optional) is the ID of the user making the request. When provided, the server verifies that the user is an active member of the organization that owns the license. If omitted, the server only validates the license key itself without checking membership.

Successful Response

When the license is valid, the API returns a JSON object with "valid" set to true, a signed token string, the plan type ("free", "pro", or "business"), the organization ID, and an expiration timestamp. The token is the most important field — it is a self-contained, cryptographically signed payload that the desktop app caches for offline use.

{
  "valid": true,
  "token": "eyJsaWNlbnNlSWQiOiIuLi4iLC...base64url.Ed25519signature",
  "type": "pro",
  "orgId": "org_abc123",
  "expiresAt": "2026-04-09T00:00:00.000Z"
}

Error Responses

When the license is invalid, the API returns "valid" set to false along with an optional "reason" field. Possible reasons include: "expired" (the license has passed its expiration date), "revoked" (the license was permanently deactivated), "suspended" (the license is temporarily suspended due to a payment issue), and "not_a_member" (a userId was provided but that user is not an active member of the license's organization). If no license matches the key, the response contains only "valid": false with no reason.

// Expired license
{ "valid": false, "reason": "expired" }

// User not in organization
{ "valid": false, "reason": "not_a_member" }

// License not found
{ "valid": false }

// Missing license key (400)
{ "error": "License key is required" }

Rate Limiting

The API enforces rate limiting of 30 requests per minute per IP address. Rate limit information is included in the response headers: X-RateLimit-Limit (always 30), X-RateLimit-Remaining (how many requests you have left in the current window), and X-RateLimit-Reset (Unix timestamp when the window resets). If you exceed the limit, you receive a 429 status code with an error message. The desktop app typically only calls this endpoint once at launch and then periodically in the background, so you should never hit this limit under normal usage.

HTTP/1.1 200 OK
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 28
X-RateLimit-Reset: 1741564800

{ "valid": true, "token": "...", ... }

If you receive a 429 response, wait until the X-RateLimit-Reset timestamp before retrying.

Token Format

The signed token uses a simple two-part format: base64url(payload).base64url(Ed25519_signature). The payload is a JSON object encoded as a base64url string. The signature is the Ed25519 digital signature of the payload string, also encoded as base64url. The two parts are separated by a dot. This format is similar to JWT but uses Ed25519 instead of RS256/HS256 and does not include a separate header.

// Token structure:
// base64url(JSON_payload) + "." + base64url(Ed25519_signature)

// Decoded payload example:
{
  "licenseId": "lic_abc123",
  "userId": "usr_def456",
  "email": "user@example.com",
  "type": "pro",
  "status": "active",
  "features": {
    "timeTracking": true,
    "clients": true,
    "projects": true,
    "tasks": true,
    "ai": true,
    "slack": true,
    "reports": true,
    "recurring": false,
    "goals": false
  },
  "issuedAt": "2026-01-15T00:00:00.000Z",
  "expiresAt": "2026-04-15T00:00:00.000Z",
  "timestamp": 1741564800000
}

Offline Verification

The desktop app embeds the Ed25519 public key in its binary. When verifying a cached token offline, the app splits the token at the dot separator, takes the payload string (everything before the dot), and verifies the signature (everything after the dot) using the embedded public key. If the signature is valid, the app decodes the base64url payload and checks the expiration date and status. This allows the app to confirm that a token was genuinely issued by the Nareli server without making any network requests.

// Offline verification pseudocode:

const [payloadB64, signatureB64] = token.split(".");

// Verify signature using embedded public key
const isValid = ed25519.verify(
  publicKey,
  base64urlDecode(signatureB64),
  payloadB64
);

if (isValid) {
  const payload = JSON.parse(base64urlDecode(payloadB64));
  // Check payload.expiresAt, payload.status, payload.features
}

The public key can safely be embedded in the app binary. Only the private key (which never leaves the server) can create valid signatures.

Error Handling Best Practices

When integrating with the license verification API, handle these cases: network errors (use the cached token if available), 429 rate limit responses (back off and retry after the reset time), 400 bad request (check that the license key format is correct), and invalid tokens (re-fetch from the server). The desktop app uses a graceful degradation strategy: if the server is unreachable, it falls back to the locally cached token. If the cached token is expired, it notifies the user but does not lock them out immediately — it allows a short grace period for the user to reconnect.

License Verification API | Nareli