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

ItemValue
Errors{ "code": "MACHINE_CODE", "error": "Human message" }
401Missing or invalid token
403Not allowed (for example ENTITLEMENT_REQUIRED_PREMIUM)
404 with *_DISABLEDFeature flag is off in this deployment
429Rate limit or plan quota, with Retry-After
Premium and sync responsesCache-Control: no-store

Public

MethodPathResponse
GET/api/health200 { "status": "ok" }
GET/api/plansThe 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

MethodPathAuthDescription
GET/api/sessionBearer{ "auth": { sub, email, name, roles, sid, gen, trusted, planTier? } }
POST/api/modelBearerBody { "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.

MethodPathBodyDescription
GET/api/premium/modelsModels and limits for your plan
GET/api/premium/usageDaily 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.

MethodPathDescription
POST/api/voicegen/attestationsRecord 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/:voiceIdDelete a voice you own

Conversation history

Enabled by JARVIS_HISTORY_SYNC_ENABLED. Bodies are end-to-end ciphertext. 120 requests per minute per user.

MethodPathDescription
GET/api/history/status{ keyId, count, latestSeq, limits }
GET/api/history?after=<seq>&limit=nCatch-up page, oldest first (max 40)
GET/api/history?before=<seq>&limit=nScroll-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/:entryIdDelete 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.

MethodPathDescription
POST/api/pairing/beginStart a session, returns the code
GET/api/pairing/:code/challengeFetch the initiator's public key
POST/api/pairing/:code/respondResponder's key and codeProof
GET/api/pairing/:code/statusPoll status
GET/api/pairing/:code/responder-keyInitiator reads the responder key
POST/api/pairing/:code/completeInitiator completes
DELETE/api/pairing/:codeClose 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.

MethodPathDescription
PUT/api/cloud-sync/:recordKeyUpsert a client-encrypted record
GET/api/cloud-sync/:recordKeyFetch one record (still ciphertext)
GET/api/cloud-syncList your record keys and timestamps
DELETE/api/cloud-sync/:recordKeyDelete one record
DELETE/api/cloud-syncErase all your records

Voice recognition consent

Enabled by JARVIS_VOICE_CONSENT_ENABLED.

MethodPathDescription
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-consentYour 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.

MethodPathDescription
GET/api/admin/secrets[/:name]Is the slot set, and its last four characters
PUT/api/admin/secrets/:nameSet a value
DELETE/api/admin/secrets/:nameRemove 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.