Skip to content

API reference

Client API

The public API your shipped software calls. It identifies your application by its public app ID and signs every response with your application's Ed25519 key.

Basics

Base path: /api/v1/client. Requests and responses are JSON. Every request must include your app ID:

Request headers
HeaderRequiredDescription
X-Aroli-AppyesYour public app ID, e.g. app_8f2k1x9q.
X-Aroli-NoncenoA random value (up to 128 characters) that is included in the response signature. Use a fresh one per request to detect replayed responses.
AuthorizationsomeBearer aus_… - the session token from register or login. Required by endpoints marked Session.

Successful responses are wrapped as { data, requestId }. Errors use { "error": { "code", "message" }, "requestId" } - see error codes.

Response signatures

Every response from the client API carries three headers:

Signature headers
HeaderFormatDescription
X-Aroli-Signaturebase64Ed25519 signature over the string ${timestamp}.${nonce}.${body}.
X-Aroli-Timestampunix secondsWhen the response was signed.
X-Aroli-Key-IdstringWhich of your application's signing keys produced the signature (for rotation).

nonce is the value you sent in X-Aroli-Nonce (empty if you sent none) and body is the exact raw response body. Verify with the public key pinned in your build and reject the response if verification fails or the timestamp is too old. The official SDKs do all of this for you.

verify.ts - manual verification (Node.js)
import { createPublicKey, verify } from "node:crypto";

// pinnedKey: your app's Ed25519 public key as shown in the dashboard (32 raw bytes, base64)
const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");

export function verifyAroli(res: Response, raw: string, nonce: string, pinnedKey: string) {
  const key = createPublicKey({
    key: Buffer.concat([SPKI_PREFIX, Buffer.from(pinnedKey, "base64")]),
    format: "der",
    type: "spki",
  });
  const ts = res.headers.get("x-aroli-timestamp") ?? "";
  const sig = Buffer.from(res.headers.get("x-aroli-signature") ?? "", "base64");
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && verify(null, Buffer.from(`${ts}.${nonce}.${raw}`), key, sig);
}

Rate limits

Limits are applied per application and client IP, with tighter limits on sign-in, registration, redemption and validation. Responses include X-RateLimit-Limit and X-RateLimit-Remaining; a 429 includes Retry-After.

Accounts and sessions

POST/api/v1/client/registerApp ID

Creates an end user, signs them in on this device and optionally redeems a license key in the same call. Returns 201.

Register body
FieldTypeDescription
usernamestringRequired. 1–64 characters.
passwordstringRequired.
emailstring?Optional email address.
licenseKeystring?Optional key to redeem immediately.
hwidstring?Hardware ID of this machine (8–512 characters).
platformstring?Free-form platform label, e.g. windows.
201 Created
{
  "data": {
    "user": { "id": "…", "username": "alice", "email": null, "status": "ACTIVE", "createdAt": "…" },
    "session": { "token": "aus_…", "expiresAt": "…" },
    "entitlements": [{ "product": "pro", "name": "Pro", "status": "ACTIVE", "active": true, "expiresAt": "…" }]
  },
  "requestId": "req_…"
}
POST/api/v1/client/loginApp ID

Signs in an existing end user. Body: username, password, optional hwid and platform. Returns { user, session, device, entitlements }, where device is the bound device (or null when no hwid was sent). Fails with invalid_credentials, device_mismatch, device_limit_reached, user_banned or user_disabled. Sign-in attempts are also throttled per username.

GET/api/v1/client/sessionSession

Validates the bearer token and returns { user, session: { expiresAt, lastHeartbeatAt }, entitlements }.

POST/api/v1/client/heartbeatSession

Keeps the session alive. Returns { ok: true, nextHeartbeatWithinSeconds }. Sessions that miss heartbeats for longer than the application's timeout end automatically.

POST/api/v1/client/logoutSession

Ends the session immediately. Returns { ok: true }.

Licenses and devices

POST/api/v1/client/licenses/redeemSession

Body: { licenseKey }. Redeems the key for the signed-in user and activates it on the session's device. Redemption is atomic: concurrent attempts can't redeem the same key twice. Returns the user's updated { entitlements }.

POST/api/v1/client/licenses/validateApp ID

License-only mode - no user account. Body: { licenseKey, hwid, platform? }. Validates the key and binds it to this device, consuming an activation slot on first use.

200 OK
{
  "data": {
    "valid": true,
    "product": "pro",
    "expiresAt": "2027-01-14T09:30:00.000Z",
    "activations": { "used": 1, "max": 3 }
  },
  "requestId": "req_…"
}
POST/api/v1/client/devices/deactivateApp ID

Body: { licenseKey, hwid }. Frees this device's activation slot on the license - for example before a user moves to a new machine. Returns { ok: true }.

Entitlements

GET/api/v1/client/entitlementsSession

Returns { entitlements: [{ product, name, status, active, expiresAt }] } for the signed-in user.

GET/api/v1/client/entitlements/{product}Session

Checks one product, e.g. /entitlements/pro. Returns { product, active, status, expiresAt }. status is ACTIVE, FROZEN, EXPIRED, REVOKED or NONE; expiresAt is null for lifetime access. See how entitlements work.

Variables and keys

GET/api/v1/client/variables/{key}App ID / Session

Reads an application variable and returns { key, value, updatedAt }. Public variables need only the app ID; authenticated variables also need a session token; private variables are never served to clients.

GET/api/v1/client/keysApp ID

Lists your application's signing public keys: { algorithm: "Ed25519", keys: [{ kid, publicKey, status, createdAt }] }. Useful for rotation tooling and debugging - a key fetched over the network must never replace the key pinned in your build.

Error codes

Errors are stable, machine-readable strings. The message is safe to show to end users. Some errors include a details object with more context.

Error response
{
  "error": { "code": "device_mismatch", "message": "…" },
  "requestId": "req_…"
}
All error codes
CodeHTTPMeaning
bad_request400The request couldn't be parsed - for example, the body isn't valid JSON.
validation_failed400The body is valid JSON but a field is missing or invalid. details lists each problem.
unauthorized401Missing or unknown X-Aroli-App header, or a required bearer token is missing.
invalid_credentials401Wrong username or password.
session_expired401The session token has expired or was revoked. Sign in again.
entitlement_missing402The user has no active entitlement for the requested product.
plan_limit_reached402The developer's Aroli plan limit was reached (for example, end users per app).
forbidden403The caller isn't allowed to perform this action.
user_banned403The end user has been banned.
user_disabled403The end user account is disabled.
device_mismatch403The account or key is bound to a different device.
license_revoked403The license key was revoked.
license_suspended403The license key is suspended.
app_inactive403The application is paused or archived and isn't accepting requests.
not_found404The requested resource doesn't exist (or isn't visible to clients).
license_invalid404The license key doesn't exist for this application.
conflict409The request conflicts with the current state - for example, a username that's already taken.
license_already_redeemed409The key has already been redeemed.
activation_limit_reached409The key has no activation slots left.
device_limit_reached409The account has no free device slots.
idempotency_conflict409An Idempotency-Key was reused with a different request (management API).
rate_limited429Too many requests. Wait for the number of seconds in Retry-After.
internal_error500Something went wrong on our side. Quote the requestId when contacting support.