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:
| Header | Required | Description |
|---|---|---|
| X-Aroli-App | yes | Your public app ID, e.g. app_8f2k1x9q. |
| X-Aroli-Nonce | no | A random value (up to 128 characters) that is included in the response signature. Use a fresh one per request to detect replayed responses. |
| Authorization | some | Bearer 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:
| Header | Format | Description |
|---|---|---|
| X-Aroli-Signature | base64 | Ed25519 signature over the string ${timestamp}.${nonce}.${body}. |
| X-Aroli-Timestamp | unix seconds | When the response was signed. |
| X-Aroli-Key-Id | string | Which 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.
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
/api/v1/client/registerApp IDCreates an end user, signs them in on this device and optionally redeems a license key in the same call. Returns 201.
| Field | Type | Description |
|---|---|---|
| username | string | Required. 1–64 characters. |
| password | string | Required. |
| string? | Optional email address. | |
| licenseKey | string? | Optional key to redeem immediately. |
| hwid | string? | Hardware ID of this machine (8–512 characters). |
| platform | string? | Free-form platform label, e.g. windows. |
{
"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_…"
}/api/v1/client/loginApp IDSigns 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.
/api/v1/client/sessionSessionValidates the bearer token and returns { user, session: { expiresAt, lastHeartbeatAt }, entitlements }.
/api/v1/client/heartbeatSessionKeeps the session alive. Returns { ok: true, nextHeartbeatWithinSeconds }. Sessions that miss heartbeats for longer than the application's timeout end automatically.
/api/v1/client/logoutSessionEnds the session immediately. Returns { ok: true }.
Licenses and devices
/api/v1/client/licenses/redeemSessionBody: { 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 }.
/api/v1/client/licenses/validateApp IDLicense-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.
{
"data": {
"valid": true,
"product": "pro",
"expiresAt": "2027-01-14T09:30:00.000Z",
"activations": { "used": 1, "max": 3 }
},
"requestId": "req_…"
}/api/v1/client/devices/deactivateApp IDBody: { 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
/api/v1/client/entitlementsSessionReturns { entitlements: [{ product, name, status, active, expiresAt }] } for the signed-in user.
/api/v1/client/entitlements/{product}SessionChecks 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
/api/v1/client/variables/{key}App ID / SessionReads 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.
/api/v1/client/keysApp IDLists 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": { "code": "device_mismatch", "message": "…" },
"requestId": "req_…"
}| Code | HTTP | Meaning |
|---|---|---|
| bad_request | 400 | The request couldn't be parsed - for example, the body isn't valid JSON. |
| validation_failed | 400 | The body is valid JSON but a field is missing or invalid. details lists each problem. |
| unauthorized | 401 | Missing or unknown X-Aroli-App header, or a required bearer token is missing. |
| invalid_credentials | 401 | Wrong username or password. |
| session_expired | 401 | The session token has expired or was revoked. Sign in again. |
| entitlement_missing | 402 | The user has no active entitlement for the requested product. |
| plan_limit_reached | 402 | The developer's Aroli plan limit was reached (for example, end users per app). |
| forbidden | 403 | The caller isn't allowed to perform this action. |
| user_banned | 403 | The end user has been banned. |
| user_disabled | 403 | The end user account is disabled. |
| device_mismatch | 403 | The account or key is bound to a different device. |
| license_revoked | 403 | The license key was revoked. |
| license_suspended | 403 | The license key is suspended. |
| app_inactive | 403 | The application is paused or archived and isn't accepting requests. |
| not_found | 404 | The requested resource doesn't exist (or isn't visible to clients). |
| license_invalid | 404 | The license key doesn't exist for this application. |
| conflict | 409 | The request conflicts with the current state - for example, a username that's already taken. |
| license_already_redeemed | 409 | The key has already been redeemed. |
| activation_limit_reached | 409 | The key has no activation slots left. |
| device_limit_reached | 409 | The account has no free device slots. |
| idempotency_conflict | 409 | An Idempotency-Key was reused with a different request (management API). |
| rate_limited | 429 | Too many requests. Wait for the number of seconds in Retry-After. |
| internal_error | 500 | Something went wrong on our side. Quote the requestId when contacting support. |