API reference

Base URL https://eam.thundera.eu. JSON in, JSON out. Every route in the server is listed here.

Authentication

ModeHowUsed by
EAM API tokenAuthorization: Bearer <token> (a bare token is also accepted). Create in Settings → API tokens.Scripts, MCP clients
EAM session tokenBearer token returned by /api/v1/auth/login or /api/v1/auth/sso-exchangeWeb app
Thundera SSO cookiethundera_token cookie (HS256 JWT issued by auth.thundera.eu, type access)Browser
OIDC access tokenRS256 token from auth.thundera.eu for the resource https://eam.thundera.eu/mcp or the API originmacOS app, OAuth MCP clients

A first-time SSO or OIDC caller is auto-provisioned. Resolution order: SSO, then OIDC, then EAM tokens.

Errors

StatusMeaning
401No credential, or invalid/expired OIDC token. Includes WWW-Authenticate: Bearer resource_metadata=….
403EAM token invalid or revoked; or System admin access required on /admin/api/*.
429Rate limited. Retry-After seconds. Body {"error":"Rate limit exceeded"} (MCP: JSON-RPC error).
other 4xx{"error":"…"}; import routes use {"error":{"code","message"}}.

Rate limits

  • Credential endpoints (/api/v1/auth/* except /me): 10 per minute per client IP.
  • /api/v1/write, /api/v1/search, /api/v1/context, /mcp: 600 per minute per credential.
  • Server settings EAM_RATE_LIMIT_AUTH_PER_MIN and EAM_RATE_LIMIT_API_PER_MIN; import has its own limits.
Public pages

Page routes are public shells that load their data through the authenticated APIs below.

Public routes

GET /health

Liveness probe.

AuthRequestResponseErrors
none—200 text ok—

GET /metrics

Prometheus metrics.

AuthRequestResponseErrors
none—Prometheus text format—

GET /api/plans

Cached proxy of the central plan catalogue.

AuthRequestResponseErrors
none—{"plans":[…]} (same shape as auth.thundera.eu, prices in cents). Cache-Control 60 s.503 {"plans":[],"error":"plans_unavailable"}

GET /api/v1/status

Service status.

AuthRequestResponseErrors
none (optional Bearer)—{service, version, status, total_blocks, users_loaded}. total_blocks is filled only when a valid EAM Bearer token is sent.—

GET /api/v1/app/release

Latest macOS release manifest.

AuthRequestResponseErrors
none—{"available":false,"message"} or {available:true, version, key, sha256, size, published_at, min_macos}—

GET /download/macos

Redirect to the latest macOS build.

AuthRequestResponseErrors
none—302 to a presigned URL (valid 1 h)404 none published; 503 temporarily unavailable

GET /.well-known/oauth-protected-resource[/mcp]

OAuth protected-resource metadata (RFC 9728).

AuthRequestResponseErrors
none—{resource, authorization_servers, scopes_supported, bearer_methods_supported, resource_name, resource_documentation}—

GET /

Landing page. Also public page shells: /app, /app/, /app/brain, /login, /admin, /settings, /app/native-auth, /ui/*. Their JavaScript calls the authenticated APIs.

AuthRequestResponseErrors
none—HTML—

GET /list

Legacy list view.

AuthRequestResponseErrors
none—301 to /app (query string kept)—

GET /favicon.ico

Empty favicon.

AuthRequestResponseErrors
none—204—

Auth routes

POST /api/v1/auth/login

Email and password login for local EAM accounts.

AuthRequestResponseErrors
none (10/min/IP){email, password}{token, user_id, email, name, role}. token is a session token usable as Bearer.401 invalid credentials

POST /api/v1/auth/sso-exchange

Exchange the thundera_token cookie for an EAM session token.

AuthRequestResponseErrors
Thundera cookie (10/min/IP)—{token, user_id, email, name, role, thundera_roles}401 no cookie / invalid SSO token; 500 auto-provision failed

GET /api/v1/auth/me

Current user.

AuthRequestResponseErrors
required—{user_id, email, name, role, plan_id, plan_name}—

POST /api/v1/auth/link-identity

Link your Thundera identity to the EAM account that owns an API token.

AuthRequestResponseErrors
Thundera SSO or OIDC (10/min/IP){api_token}{linked:true, user_id, email, identity:{issuer,email}}400 not signed in with Thundera; 403 token missing, invalid, or a session token

GET /api/v1/tokens

List your tokens.

AuthRequestResponseErrors
required—{tokens:[{token_id, name, prefix, created_at, last_used, revoked}]}—

POST /api/v1/tokens

Create a token. Revokes all your existing active tokens first.

AuthRequestResponseErrors
required{name?} (default api-token){token_id, token, prefix, name, warning}. token is shown once.—

DELETE /api/v1/tokens/{token_id}

Revoke one of your tokens.

AuthRequestResponseErrors
required—{revoked:true, token_id}404 not found or not yours

GET /api/v1/backup

Export ACTIVE memories as a JSON download.

AuthRequestResponseErrors
required—{version:"1.0", exported_at, user_id, email, total_memories, memories:[{id, content, source_type, tags, pinned, relevance_score, access_count, created_at, entity_ids, supersedes, superseded_by, chain_depth}]}500 store unavailable

POST /api/v1/restore

Re-write memories from a backup.

AuthRequestResponseErrors
required{memories:[{content, source_type?, tags?}]}{imported, skipped, total}400 no memories

POST /api/v1/wipe

Soft-delete ALL active memories.

AuthRequestResponseErrors
required{"confirm":"WIPE ALL MEMORIES"}{wiped:true, deleted_count, user_id}400 confirmation missing; 500 store unavailable

Memories routes

POST /api/v1/write

Write a memory.

AuthRequestResponseErrors
required (600/min){content, source_type?=TEXT, tags?[]}{memory_id, persisted, salience_score, …}. For SCREEN_EVENT also reinforced and reason when rejected or merged into an existing memory.400 content required

POST /api/v1/search

Search or browse.

AuthRequestResponseErrors
required (600/min){query, top_k?=5, sort?=relevance|newest|oldest, since?, until?, include_recent?} (since/until epoch ms; browse mode top_k default 200, max 1000){results, memories (deprecated alias), query_time_ms}; browse adds mode:"browse"; UUID lookup adds lookup_type:"id"—

GET /api/v1/memories

Paginated list for the memory browser.

AuthRequestResponseErrors
requiredQuery: limit 1–200 (50), cursor, q, source (screen,web,audio,clipboard), tags (comma list), app, since/until (ISO 8601 or epoch ms), sort newest|salience, include_recent{items:[{id, content, source_type, tags, app, created_at, salience}], next_cursor, total_estimate}400 invalid limit/sort/source/cursor/time

GET /api/v1/memories/{id}

One ACTIVE memory.

AuthRequestResponseErrors
required—{id, content, source_type, tags, app, created_at, salience, pinned, status, supersedes}404 not found/not active; 409 edited (superseded_by returned); 503 store unavailable

PATCH /api/v1/memories/{id}

Edit tags in place and/or content as a new superseding memory.

AuthRequestResponseErrors
required{tags?[], content?} (at least one; max 32 tags of 64 chars, no commas; content ≤ 100,000 chars){item, previous_id} (previous_id null when nothing superseded)400 invalid; 404; 409 edited; 422 edit not stored

DELETE /api/v1/memories/{id}

Soft delete.

AuthRequestResponseErrors
required—{deleted:true, id}404; 409 pinned or edited

POST /api/v1/memories/{id}/restore

Undo a soft delete.

AuthRequestResponseErrors
required—{restored:true, item}404 not soft-deleted

POST /api/v1/memories/purge

Permanently delete soft-deleted, unpinned memories.

AuthRequestResponseErrors
required{memory_ids:[1..500 strings], dry_run?=false}{dry_run, requested, eligible, purged, purged_ids, skipped:{not_found, not_soft_deleted}, external_cleanup_failures}400 invalid ids/dry_run

GET /api/v1/tags

Your tags with counts.

AuthRequestResponseErrors
requiredQuery: limit 1–200 (50){tags:[{tag, count}]}400 invalid limit

POST /api/v1/context

Task context and HyperFocus.

AuthRequestResponseErrors
required (600/min){task_description, top_k?=10}{task_description, results, …}400 task_description required

POST /api/v1/pin

Pin or unpin.

AuthRequestResponseErrors
required{memory_id, pin?=true}{pinned, memory_id}400 memory_id required

POST /api/v1/delete

Soft-delete one memory (legacy form of DELETE).

AuthRequestResponseErrors
required{memory_id}{deleted:true, memory_id}400 missing id or pinned; 404

POST /api/v1/delete-chain

Soft-delete a memory and its supersede chain; pinned skipped.

AuthRequestResponseErrors
required{memory_id}{deleted_count, deleted_ids, skipped_pinned}400; 500 store unavailable

POST /api/v1/edit

Edit as a new superseding memory (legacy form of PATCH). Adds tag edited.

AuthRequestResponseErrors
required{memory_id, content}{edited:true, old_id, new_id, new_content}400 missing fields or not stored; 404

POST /api/v1/resolve

Resolve a HyperFocus task.

AuthRequestResponseErrors
required{task_id}{resolved:true, task_id}—

GET /api/v1/health

Store health.

AuthRequestResponseErrors
required—{score, state, total_blocks, block_limit_state, components:{embedder, qdrant, s3_vault, brain_fingerprint, …}}—

GET /api/v1/graph

Memory graph.

AuthRequestResponseErrors
requiredQuery: center, hops (3, max 5), limit (150, max 500), semantic (false){nodes, edges, center, total_memories, loaded, max_hops, hyperfocus}—

DREAM routes

GET /api/v1/dream/settings

Read settings.

AuthRequestResponseErrors
required—{enabled, interval_hours, recent_ttl_days}—

PUT /api/v1/dream/settings

Change settings.

AuthRequestResponseErrors
required{enabled?, interval_hours? 6|12|24, recent_ttl_days? 1..90, timezone?}as GET400 invalid value

POST /api/v1/dream/run

Run DREAM now.

AuthRequestResponseErrors
required{dry_run?=true, since_ms?, until_ms?}Run result (summaries consumed/created). A real run also returns expired_stale400 invalid body

GET /api/v1/dream/status

Scheduler status.

AuthRequestResponseErrors
required—{enabled, interval_hours, last_run_ms, next_due_ms, recent_pending, history}—

Import routes

GET /api/v1/import/sources

Sources and settings.

AuthRequestResponseErrors
required—{sources:[{id, label, enabled, since_days, …}]}—

GET /api/v1/import/settings

Per-source settings.

AuthRequestResponseErrors
required—{sources:{<id>:{enabled, since_days}}}—

PUT /api/v1/import/settings

Enable/disable sources.

AuthRequestResponseErrors
required{sources:{<id>:{enabled?, since_days? 1..365}}}as GET400 {error:{code,message}}

POST /api/v1/import/preview

Dry run.

AuthRequestResponseErrors
required{source, items[≤500]} (≤ 1 MB){received, accepted, duplicates, redacted, rejected:[{index,reason}], skipped_denylist, sample:[{index,text_masked}]}400; 413

POST /api/v1/import/commit

Write items.

AuthRequestResponseErrors
required{source, items[≤500], idempotency_key?}Job {job_id, status, source, received, stored, duplicates, redacted, rejected, error, created_at, finished_at, skipped_denylist}400; 403 source_not_enabled; 413; 429

GET /api/v1/import/jobs/{job_id}

Job status.

AuthRequestResponseErrors
required—Job (above)404 not_found

MCP routes

POST /mcp (also /mcp/)

Streamable HTTP MCP: initialize, tools/list, tools/call, batches.

AuthRequestResponseErrors
required (600/min)JSON-RPC 2.0 message or arrayJSON-RPC response; tool results as content[{type:text,text}]401 JSON-RPC error with WWW-Authenticate; 400 parse error; 429 rate limit

GET /mcp

Not used (stateless server).

AuthRequestResponseErrors
required—405—

Admin (system roles) routes

GET /admin/api/users

List users.

AuthRequestResponseErrors
admin—{users:[{user_id,email,name,role,company_id,company_name,plan_id,plan_name,created_at,last_login,disabled}]}403 not a system admin

POST /admin/api/users

Create a user and a default token.

AuthRequestResponseErrors
admin{email, password, name?, role?, company_id?, plan_id?}{user_id, email, token, warning}400

POST /admin/api/users/{user_id}/update

Update fields.

AuthRequestResponseErrors
adminJSON fields to update{updated:true, user_id, name, email, role, plan_id, company_id}—

POST /admin/api/users/{user_id}/disable

Disable a user.

AuthRequestResponseErrors
admin—{disabled:true, user_id}—

POST /admin/api/users/{user_id}/enable

Enable a user.

AuthRequestResponseErrors
admin—{enabled:true, user_id}—

GET /admin/api/users/{user_id}/tokens

List a user's tokens.

AuthRequestResponseErrors
admin—{tokens:[…]}—

GET /admin/api/users/{user_id}/memories

Up to 50 anonymized, 200-char-truncated memories.

AuthRequestResponseErrors
admin—{memories:[{id, content, source_type, score}], count}—

DELETE /admin/api/tokens/{token_id}

Revoke any token.

AuthRequestResponseErrors
admin—{revoked:true, token_id}404

GET /admin/api/companies

List companies.

AuthRequestResponseErrors
admin—{companies:[{company_id,name,plan_id,billing_email,created_at,disabled}]}—

POST /admin/api/companies

Create a company.

AuthRequestResponseErrors
admin{name, billing_email?, plan_id?}{company_id, name}400 name required

POST /admin/api/companies/{company_id}/update

Update a company.

AuthRequestResponseErrors
adminJSON fields{updated:true, company_id}—

GET /admin/api/plans

List plans.

AuthRequestResponseErrors
admin—{plans:[{plan_id,name,description,price_monthly,price_yearly,max_memories,max_tokens,features,active,sort_order}]}—

POST /admin/api/plans

Create a plan.

AuthRequestResponseErrors
admin{name, description?, price_monthly?, price_yearly?, max_memories?, max_tokens?, features?, sort_order?}{plan_id, name}400 name required

POST /admin/api/plans/{plan_id}/update

Update a plan.

AuthRequestResponseErrors
adminJSON fields{updated:true, plan_id}—