API reference
Base URL https://eam.thundera.eu. JSON in, JSON out. Every route in the server is listed here.
Authentication
| Mode | How | Used by |
|---|---|---|
| EAM API token | Authorization: Bearer <token> (a bare token is also accepted). Create in Settings → API tokens. | Scripts, MCP clients |
| EAM session token | Bearer token returned by /api/v1/auth/login or /api/v1/auth/sso-exchange | Web app |
| Thundera SSO cookie | thundera_token cookie (HS256 JWT issued by auth.thundera.eu, type access) | Browser |
| OIDC access token | RS256 token from auth.thundera.eu for the resource https://eam.thundera.eu/mcp or the API origin | macOS app, OAuth MCP clients |
A first-time SSO or OIDC caller is auto-provisioned. Resolution order: SSO, then OIDC, then EAM tokens.
Errors
| Status | Meaning |
|---|---|
401 | No credential, or invalid/expired OIDC token. Includes WWW-Authenticate: Bearer resource_metadata=…. |
403 | EAM token invalid or revoked; or System admin access required on /admin/api/*. |
429 | Rate 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_MINandEAM_RATE_LIMIT_API_PER_MIN; import has its own limits.
Page routes are public shells that load their data through the authenticated APIs below.
Public routes
GET /health
Liveness probe.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | 200 text ok | — |
GET /metrics
Prometheus metrics.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | Prometheus text format | — |
GET /api/plans
Cached proxy of the central plan catalogue.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | {"available":false,"message"} or {available:true, version, key, sha256, size, published_at, min_macos} | — |
GET /download/macos
Redirect to the latest macOS build.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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).
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | HTML | — |
GET /list
Legacy list view.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | 301 to /app (query string kept) | — |
GET /favicon.ico
Empty favicon.
| Auth | Request | Response | Errors |
|---|---|---|---|
| none | — | 204 | — |
Auth routes
POST /api/v1/auth/login
Email and password login for local EAM accounts.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {revoked:true, token_id} | 404 not found or not yours |
GET /api/v1/backup
Export ACTIVE memories as a JSON download.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {memories:[{content, source_type?, tags?}]} | {imported, skipped, total} | 400 no memories |
POST /api/v1/wipe
Soft-delete ALL active memories.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | Query: 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {deleted:true, id} | 404; 409 pinned or edited |
POST /api/v1/memories/{id}/restore
Undo a soft delete.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {restored:true, item} | 404 not soft-deleted |
POST /api/v1/memories/purge
Permanently delete soft-deleted, unpinned memories.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | Query: limit 1–200 (50) | {tags:[{tag, count}]} | 400 invalid limit |
POST /api/v1/context
Task context and HyperFocus.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required (600/min) | {task_description, top_k?=10} | {task_description, results, …} | 400 task_description required |
POST /api/v1/pin
Pin or unpin.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {memory_id, pin?=true} | {pinned, memory_id} | 400 memory_id required |
POST /api/v1/delete
Soft-delete one memory (legacy form of DELETE).
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {task_id} | {resolved:true, task_id} | — |
GET /api/v1/health
Store health.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {score, state, total_blocks, block_limit_state, components:{embedder, qdrant, s3_vault, brain_fingerprint, …}} | — |
GET /api/v1/graph
Memory graph.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | Query: 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {enabled, interval_hours, recent_ttl_days} | — |
PUT /api/v1/dream/settings
Change settings.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {enabled?, interval_hours? 6|12|24, recent_ttl_days? 1..90, timezone?} | as GET | 400 invalid value |
POST /api/v1/dream/run
Run DREAM now.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {dry_run?=true, since_ms?, until_ms?} | Run result (summaries consumed/created). A real run also returns expired_stale | 400 invalid body |
GET /api/v1/dream/status
Scheduler status.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {enabled, interval_hours, last_run_ms, next_due_ms, recent_pending, history} | — |
Import routes
GET /api/v1/import/sources
Sources and settings.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {sources:[{id, label, enabled, since_days, …}]} | — |
GET /api/v1/import/settings
Per-source settings.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | {sources:{<id>:{enabled, since_days}}} | — |
PUT /api/v1/import/settings
Enable/disable sources.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | {sources:{<id>:{enabled?, since_days? 1..365}}} | as GET | 400 {error:{code,message}} |
POST /api/v1/import/preview
Dry run.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | Job (above) | 404 not_found |
MCP routes
POST /mcp (also /mcp/)
Streamable HTTP MCP: initialize, tools/list, tools/call, batches.
| Auth | Request | Response | Errors |
|---|---|---|---|
| required (600/min) | JSON-RPC 2.0 message or array | JSON-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).
| Auth | Request | Response | Errors |
|---|---|---|---|
| required | — | 405 | — |
Admin (system roles) routes
GET /admin/api/users
List users.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | {email, password, name?, role?, company_id?, plan_id?} | {user_id, email, token, warning} | 400 |
POST /admin/api/users/{user_id}/update
Update fields.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | JSON fields to update | {updated:true, user_id, name, email, role, plan_id, company_id} | — |
POST /admin/api/users/{user_id}/disable
Disable a user.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {disabled:true, user_id} | — |
POST /admin/api/users/{user_id}/enable
Enable a user.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {enabled:true, user_id} | — |
GET /admin/api/users/{user_id}/tokens
List a user's tokens.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {tokens:[…]} | — |
GET /admin/api/users/{user_id}/memories
Up to 50 anonymized, 200-char-truncated memories.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {memories:[{id, content, source_type, score}], count} | — |
DELETE /admin/api/tokens/{token_id}
Revoke any token.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {revoked:true, token_id} | 404 |
GET /admin/api/companies
List companies.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | — | {companies:[{company_id,name,plan_id,billing_email,created_at,disabled}]} | — |
POST /admin/api/companies
Create a company.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | {name, billing_email?, plan_id?} | {company_id, name} | 400 name required |
POST /admin/api/companies/{company_id}/update
Update a company.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | JSON fields | {updated:true, company_id} | — |
GET /admin/api/plans
List plans.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| 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.
| Auth | Request | Response | Errors |
|---|---|---|---|
| admin | JSON fields | {updated:true, plan_id} | — |