API reference
Base URL https://jarvis.thundera.eu/api. JSON in, JSON out.
Authentication
Every route except GET /api/health and GET /api/plans needs Authorization: Bearer <access token>. Tokens are RS256 OIDC tokens issued by https://auth.thundera.eu (client jarvis-web, PKCE S256), verified against the issuer's JWKS. The audience must contain https://jarvis.thundera.eu/api.
A failure answers 401 with an RFC 6750 challenge:
WWW-Authenticate: Bearer realm="jarvis", resource_metadata="https://jarvis.thundera.eu/.well-known/oauth-protected-resource/api"
Discovery metadata (RFC 9728): GET /.well-known/oauth-protected-resource/api.
The user is always the token's sub. A user id in a body or query is never read.
Conventions
| Item | Value |
|---|---|
| Errors | { "code": "MACHINE_CODE", "error": "Human message" } |
401 | Missing or invalid token |
403 | Not allowed (for example ENTITLEMENT_REQUIRED_PREMIUM) |
404 with *_DISABLED | Feature flag is off in this deployment |
429 | Rate limit or plan quota, with Retry-After |
| Premium and sync responses | Cache-Control: no-store |
Public
| Method | Path | Response |
|---|---|---|
| GET | /api/health | 200 { "status": "ok" } |
| GET | /api/plans | The Jarvis plans from the central catalog, same JSON as https://auth.thundera.eu/api/v1/auth/billing/plans?app=jarvis. CORS *, cached 60 s, 503 if never loaded |
Session and models
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/session | Bearer | { "auth": { sub, email, name, roles, sid, gen, trusted, planTier? } } |
| POST | /api/model | Bearer | Body { "model": "<id>" }. 200 { "model": id } for a known id. 400 with code and known: [...] for an unknown id or auto. There is no silent fallback to a near-miss id |
Premium models
Disabled unless the deployment sets JARVIS_PREMIUM_GATEWAY_ENABLED. Plan limits apply. Models are the gateway aliases models:cheap, models:fast, models:smart and models:reasoning; Thundera's model gateway chooses the vendor model behind each.
| Method | Path | Body | Description |
|---|---|---|---|
| GET | /api/premium/models | Models and limits for your plan | |
| GET | /api/premium/usage | Daily and monthly consumption against limits | |
| POST | /api/premium/chat | { model, messages, system?, temperature?, maxTokens? } | One non-streaming reply |
| POST | /api/premium/decide | { model: "jev:jev-latest", state, questions } | Decision-engine call |
| POST | /api/premium/tts | { text, ... } (max 5000 chars) | Streams premium voice audio. 30 requests per minute |
Quota errors are 429 with Retry-After and resetsAt. 429 AI_BUDGET_EXCEEDED means your monthly AI budget is spent.
Cloned voices
Enabled by JARVIS_VOICEGEN_ENABLED. Requires a recorded attestation.
| Method | Path | Description |
|---|---|---|
| POST | /api/voicegen/attestations | Record a typed attestation (no audio, no message text) |
| POST | /api/premium/voicegen/synthesize | { text, voiceId, attestationId }, returns audio/mpeg. A content guardrail runs on the text |
| POST | /api/premium/voicegen/voices?name=&attestationId= | Multipart audio (max 40 MB) streamed to the provider, never stored |
| DELETE | /api/premium/voicegen/voices/:voiceId | Delete a voice you own |
Conversation history
Enabled by JARVIS_HISTORY_SYNC_ENABLED. Bodies are end-to-end ciphertext. 120 requests per minute per user.
| Method | Path | Description |
|---|---|---|
| GET | /api/history/status | { keyId, count, latestSeq, limits } |
| GET | /api/history?after=<seq>&limit=n | Catch-up page, oldest first (max 40) |
| GET | /api/history?before=<seq>&limit=n | Scroll-back page. No cursor returns the newest page |
| POST | /api/history | { entries: [{ id, deviceId, keyId, iv, ciphertext }] }, up to 20 entries of 12 KiB. Idempotent per entry id. 409 KEY_ID_MISMATCH if the key differs from your canonical one |
| DELETE | /api/history/:entryId | Delete one turn |
| DELETE | /api/history[?resetKey=1] | Erase every turn |
Overload answers 503 with Retry-After.
Device pairing
Enabled with history sync or cloud sync. Authenticated, not premium.
| Method | Path | Description |
|---|---|---|
| POST | /api/pairing/begin | Start a session, returns the code |
| GET | /api/pairing/:code/challenge | Fetch the initiator's public key |
| POST | /api/pairing/:code/respond | Responder's key and codeProof |
| GET | /api/pairing/:code/status | Poll status |
| GET | /api/pairing/:code/responder-key | Initiator reads the responder key |
| POST | /api/pairing/:code/complete | Initiator completes |
| DELETE | /api/pairing/:code | Close the session |
Cloud secret sync
Enabled by CLOUD_SYNC_ENABLED. Reading and writing need a paid plan: free callers get 403 ENTITLEMENT_REQUIRED_PREMIUM. Deleting is always allowed.
| Method | Path | Description |
|---|---|---|
| PUT | /api/cloud-sync/:recordKey | Upsert a client-encrypted record |
| GET | /api/cloud-sync/:recordKey | Fetch one record (still ciphertext) |
| GET | /api/cloud-sync | List your record keys and timestamps |
| DELETE | /api/cloud-sync/:recordKey | Delete one record |
| DELETE | /api/cloud-sync | Erase all your records |
Voice recognition consent
Enabled by JARVIS_VOICE_CONSENT_ENABLED.
| Method | Path | Description |
|---|---|---|
| POST | /api/voice-consent | { decision, textVersion, attestedHousehold, appVersion, locale? }. Any other key, and any biometric-looking key, is refused with 400. 20 per hour |
| GET | /api/voice-consent | Your latest consent state (?history=1 for your own history) |
System administration
Requires the system-super-admin or system-admin role (403 otherwise). Enabled by JARVIS_ADMIN_API_ENABLED. Secret values are write-only: only the last four characters are ever returned.
| Method | Path | Description |
|---|---|---|
| GET | /api/admin/secrets[/:name] | Is the slot set, and its last four characters |
| PUT | /api/admin/secrets/:name | Set a value |
| DELETE | /api/admin/secrets/:name | Remove it |
| GET, PUT, DELETE | /api/admin/config[/:key] | Read, set or reset a configuration key |
Secret slots: elevenlabs_api_key and jev_api_key. Model access goes through models.thundera.eu and needs no key here.
Memory graph
GET /api/memory/graph returns { nodes, links, metadata }. It is currently an empty placeholder; the desktop app reads EAM directly.