Desktop API reference
The REST endpoints the AUTO/PILOT desktop app uses for auth, activation and updates.
All endpoints live under /api/v1, speak JSON, and return
{ "ok": true, ...payload }
{ "ok": false, "error": { "code": "ERROR_CODE", "message": "Human readable" } }
Authenticated calls use Authorization: Bearer <sessionToken>. Cookies are not accepted
on this surface, which is what keeps it free of CSRF concerns.
Error codes
| Code | HTTP | Meaning |
|---|---|---|
VALIDATION_FAILED |
422 | Body failed schema validation; see issues. |
INVALID_CREDENTIALS |
401 | Wrong email or password. |
EMAIL_NOT_VERIFIED |
403 | Confirm the address on the website first. |
ACCOUNT_DISABLED |
403 | Account disabled by an administrator. |
UNAUTHORIZED |
401 | Missing, invalid or expired bearer token. |
LICENSE_REQUIRED |
403 | No non-revoked license pack on the account. |
ACTIVATION_LIMIT_REACHED |
409 | All slots in use. Free one or buy a pack. |
DEVICE_DEACTIVATED |
403 | This activation was released; re-activate. |
DEVICE_CLAIMED_BY_OTHER_ACCOUNT |
409 | Device token belongs to another account. |
RATE_LIMITED |
429 | Back off; see retryAfterSeconds. |
SERVER_MISCONFIGURED |
503 | Server is missing keys or storage config. |
POST /api/v1/auth/login
Signs in and, when deviceToken and deviceName are present, activates the device in
the same call.
{
"email": "dj@example.com",
"password": "…",
"deviceName": "booth-mbp",
"deviceToken": "9f3c…opaque-32-bytes-or-more",
"platform": "mac",
"appVersion": "1.0.3"
}
Response:
{
"ok": true,
"sessionToken": "…",
"sessionExpiresAt": "2026-03-01T12:00:00.000Z",
"licenseToken": "eyJhbGciOiJSUzI1NiIs…",
"licenseTokenExpiresAt": "2026-01-30T12:00:00.000Z",
"user": { "id": "…", "email": "dj@example.com", "role": "user", "emailVerified": true },
"entitlement": {
"licensed": true,
"packs": 1,
"maxActivations": 3,
"activeActivations": 1,
"remainingActivations": 2
},
"device": { "id": "…", "name": "booth-mbp", "active": true }
}
Generate deviceToken once, locally, from a CSPRNG (32 bytes, URL-safe) and store it in
the app data directory. Never derive it from a hardware id — the server only stores its
SHA-256 hash and cannot recover it for you.
POST /api/v1/auth/refresh
Called during update checks. Returns a fresh licenseToken and the current entitlement.
Rejects with LICENSE_REQUIRED, DEVICE_DEACTIVATED or ACCOUNT_DISABLED when the
account state has changed since last time. Optional body:
{ "appVersion": "1.0.4", "deviceToken": "…" }
POST /api/v1/auth/logout
Ends the bearer session. The activation is not released — use the deactivate endpoint for that.
GET /api/v1/me
Account, role, entitlement, current device and tokenVersion.
GET /api/v1/devices
Every activation on the account. The one matching the calling session is flagged
"current": true.
POST /api/v1/devices/activate
{ "deviceName": "backup-laptop", "deviceToken": "…", "platform": "windows", "appVersion": "1.0.3" }
Enforces the license and the activation limit, then binds the calling session to the device and returns a fresh license token.
POST /api/v1/devices/deactivate
{ "deviceId": "…" }
or
{ "deviceToken": "…" }
Frees the slot. If the deactivated device is the caller, sessionInvalidated is true
and the app should discard its cached license token.
GET /api/v1/builds/latest
GET /api/v1/builds/latest?platform=mac&channel=stable
Returns build metadata plus a short-lived presigned download.url for licensed
accounts. Unlicensed accounts get LICENSE_REQUIRED with the metadata attached but no
URL.
GET /api/v1/license/public-key
Public verification material for offline license tokens — JWKS by default, PEM with
?format=pem. Pin it in the client; do not fetch it per verification.
Rate limits
Login and activation are rate limited per IP (and per account for activation). A 429
carries retryAfterSeconds — respect it with backoff rather than retrying in a loop.