Authenticate to the API
NovaVMS accepts two credential types on /api/v1/*: a service-account API key (for server-to-server integrations) and a user JWT access token (for customer-facing apps acting on behalf of a human). Both travel in the Authorization: Bearer ... header. Pick one per integration — do not mix.
Service-account API keys (recommended for integrations)
A service-account key represents a non-human caller with a fixed role and scope. It does not expire on a timer and it does not carry a session. Use this for ticketing bridges, SIEM collectors, backup exporters, and anything else where there is no logged-in user.
Get a key. An admin or owner mints the key from the admin UI at /admin/service-accounts. See Rotate an API key for the exact UI flow. The key is shown once; copy it straight into your secret store.
Send the key. Put it in the Authorization header on every request (since v1.0):
curl -H "Authorization: Bearer sk_live_abc123" \ https://novavms.novalien.com/api/v1/cameras// TypeScript (@novavms/sdk >= 1.0.0)import { createClient } from '@novavms/sdk';const novavms = createClient({ apiKey: 'sk_live_abc123' });# Python (novavms >= 1.0.0)from novavms import Clientnovavms = Client(api_key="sk_live_abc123")Key prefixes. sk_live_ is a production key. sk_test_ is a sandbox-org key and will return 404 on production-only resources. See API keys for the full prefix and quota rules.
User JWT (for a customer app calling on behalf of a user)
Use this when a human signs in through your product and you call NovaVMS with their identity. The flow is email + password exchanged for a short-lived access token plus a longer-lived refresh token.
1. Login (POST /api/v1/auth/login) — available since v1.0. The client_type field is web or mobile (D48). Use mobile so NovaVMS returns the refresh token in the JSON body rather than as an HttpOnly cookie (cookies are only useful for browser clients):
curl -X POST https://novavms.novalien.com/api/v1/auth/login \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "password": "CorrectHorseBattery!", "client_type": "mobile", "remember_me": false }'const res = await fetch('https://novavms.novalien.com/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password: 'CorrectHorseBattery!', client_type: 'mobile', }),});const { access_token, refresh_token } = await res.json();import httpxr = httpx.post( "https://novavms.novalien.com/api/v1/auth/login",)tokens = r.json() # {"access_token": "...", "refresh_token": "..."}2. Call the API with the access token:
curl -H "Authorization: Bearer $ACCESS_TOKEN" \ https://novavms.novalien.com/api/v1/cameras3. Refresh before the access token expires (POST /api/v1/auth/refresh). Send the refresh token in the JSON body for client_type=mobile; for client_type=web the cookie travels automatically and you do not need to set the token:
curl -X POST https://novavms.novalien.com/api/v1/auth/refresh \ -H "Content-Type: application/json" \ -d '{"refresh_token":"'"$REFRESH_TOKEN"'","client_type":"mobile"}'The server rotates the refresh token on every refresh — store the new one from the response (mobile) or trust the rotated cookie (web) and discard the old.
Token lifetimes
| Token | Default TTL | With remember_me: true | Notes |
|---|---|---|---|
| Access token (JWT) | 1 hour | 1 hour | Stateless; cannot be revoked mid-lifetime |
| Refresh token | 7 days | 30 days | Opaque; stored hashed server-side; rotated each refresh |
| Service-account API key | No expiry | — | Revoke via admin UI or set expires_at explicitly |
User-Agent binding. Refresh tokens are bound to the User-Agent string captured at login. A refresh attempt with a different User-Agent returns 401 SESSION_MISMATCH and the user must re-login. No IP binding — that would break mobile clients, VPN users, and anyone behind a CGNAT. This is decision D-R2.
Related
- Roles and permissions — what a token can do once authenticated
- Rotate an API key — admin-side flow for issuing and revoking keys
- Error codes — meaning of
401 UNAUTHORIZED,401 SESSION_MISMATCH,423 ACCOUNT_LOCKED