(HTTP Endpoints for Frontend and External Clients)
This document covers:
- All HTTP endpoints and their parameters
- Request/response formats
- Authentication requirements
- Error handling
- Rate limiting This document does NOT cover:
- MCP tools (see
docs/specs/MCP_SPEC.md) - WebSocket connections
- Internal service-to-service communication
Development:
http://localhost:3080
Production:
https://neotoma.fly.dev
All endpoints (except /health, /openapi.yaml, and MCP OAuth public endpoints) require Bearer token authentication:
Authorization: Bearer <NEOTOMA_BEARER_TOKEN>Token Configuration:
- Set via
NEOTOMA_BEARER_TOKENenvironment variable (preferred) or legacyACTIONS_BEARER_TOKEN - Use strong random token in production
- Never commit tokens to git
- When encryption is enabled (
NEOTOMA_ENCRYPTION_ENABLED=true), use the key-derived MCP token instead:neotoma auth mcp-token
OAuth endpoints for MCP client authentication (no bearer token required for public endpoints).
A client with no credentials reaches an authenticated session in four requests. Step 2 returns 200 with no credential — that is required by RFC 9728 §3, not a bug. The discovery documents exist to tell a caller holding nothing where to authenticate, so gating them would deadlock the handshake: the WWW-Authenticate challenge in step 1 points at the step-2 document.
POST /mcpwith no credential → 401 carryingWWW-Authenticate: Bearer resource_metadata="<base>/.well-known/oauth-protected-resource".GETthat URL, noAuthorizationand noX-Connection-Id→ 200 withresourceand a non-emptyauthorization_servers.GET /.well-known/oauth-authorization-server→ 200 with the authorization and token endpoints.- Complete the OAuth flow against those endpoints, then retry
/mcpwithAuthorization: Bearer <token>.
Verify the public documents:
curl -sI https://<instance>/.well-known/oauth-protected-resource # expect 200
curl -sI https://<instance>/.well-known/oauth-protected-resource/mcp # expect 200
curl -sI https://<instance>/.well-known/oauth-authorization-server # expect 200
curl -sI https://<instance>/.well-known/openid-configuration # expect 404/.well-known/oauth-protected-resource/mcp is the RFC 9728 §3.1 resource-suffixed form, served identically — a client whose resource is /mcp probes that path rather than the bare one, and either probe order works.
/.well-known/openid-configuration returns 404: this server does not implement OIDC discovery, and a 404 correctly makes an OIDC-first client fall back to the RFC 8414 document. It is deliberately not a 401 (which would claim the endpoint exists and send the client into a dead end) and not a synthetic 200 alias (which would advertise a discovery contract the server does not implement).
One legitimate 401 remains on the protected-resource paths: a request carrying a stale or unrecognized X-Connection-Id gets 401 invalid_token, because that caller asserted a credential. Any resource_metadata URL named in that response still resolves 200 unauthenticated.
Start OAuth authorization flow for MCP client.
Endpoint: POST /mcp/oauth/initiate
Request:
{
"connection_id": "cursor-2025-01-21-abc123",
"client_name": "Cursor"
}Parameters:
connection_id: Unique identifier for this connection (required)client_name: Name of MCP client (optional, e.g., "Cursor", "Claude Code")
Response: 200 OK
{
"auth_url": "https://your-server/mcp/oauth/authorize?...",
"connection_id": "cursor-2025-01-21-abc123",
"expires_at": "2025-01-21T10:10:00Z"
}Errors:
400: Missing or invalid connection_id500: Failed to create OAuth state
Handles OAuth authorization callback.
Endpoint: GET /mcp/oauth/callback
Query Parameters:
code: Authorization code from OAuth provider (required)state: OAuth state token (required)
Response: Redirects to frontend with connection status
- Success:
{FRONTEND_URL}/mcp-setup?connection_id={id}&status=success - Error:
{FRONTEND_URL}/mcp-setup?status=error&error={message}
Errors:
400: Missing code or state500: Token exchange or storage failed
Check status of MCP OAuth connection.
Endpoint: GET /mcp/oauth/status
Query Parameters:
connection_id: Connection identifier (required)
Response: 200 OK
{
"status": "active",
"connection_id": "cursor-2025-01-21-abc123"
}Status Values:
pending: OAuth flow initiated but not completedactive: Connection active and ready to useexpired: Connection revoked or OAuth flow timed out
Errors:
400: Missing connection_id500: Failed to query status
List user's active MCP OAuth connections (authenticated).
Endpoint: GET /mcp/oauth/connections
Headers:
Authorization: Bearer <SESSION_TOKEN>Response: 200 OK
{
"connections": [
{
"connectionId": "cursor-2025-01-21-abc123",
"clientName": "Cursor",
"createdAt": "2025-01-21T10:00:00Z",
"lastUsedAt": "2025-01-21T11:30:00Z"
}
]
}Errors:
401: Missing or invalid bearer token500: Failed to fetch connections
Revoke an OAuth connection (authenticated).
Endpoint: DELETE /mcp/oauth/connections/:connection_id
Headers:
Authorization: Bearer <SESSION_TOKEN>Response: 200 OK
{
"success": true
}Errors:
401: Missing or invalid bearer token500: Failed to revoke connection
Use MCP actions instead:
store()- For storing source (unstructured files or structured data)correct()- For correctionsmerge_entities()- For entity mergingretrieve_entities()- For querying entities- See
docs/specs/MCP_SPEC.mdfor complete MCP action reference
Query records with filters and search.
Endpoint: POST /retrieve_records
Request:
{
"type": "note",
"properties": {
"tags": ["important"]
},
"search": ["meeting", "notes"],
"search_mode": "both",
"limit": 20,
"offset": 0,
"include_total_count": true
}Response: 200 OK
{
"records": [
{
"id": "uuid-1",
"type": "note",
"properties": { "text": "Meeting notes", "tags": ["important"] }
}
],
"total_count": 42
}Parameters:
type: Filter by record type (optional)properties: Filter by property values (optional, JSON object)search: Array of search terms (optional)search_mode:"keyword","semantic", or"both"(default:"both")limit: Maximum results (default: 100, max: 1000)offset: Pagination offset (default: 0)include_total_count: Include total count in response (default: false) Errors:400: Invalid request (invalid search_mode)401: Missing or invalid bearer token
Upload a file and optionally create/update a record.
Endpoint: POST /upload_file
Content-Type: multipart/form-data
Form Fields:
file: File to upload (required)record_id: Existing record ID to attach file to (optional)properties: JSON string of properties (optional)bucket: Storage bucket name (optional, default:files) Request Example:
curl -X POST http://localhost:3080/upload_file \
-H "Authorization: Bearer $TOKEN" \
-F "file=@document.pdf" \
-F "properties={\"title\":\"My Document\"}"Response: 201 Created or 200 OK
{
"id": "uuid-here",
"type": "pdf_document",
"properties": {
"title": "My Document",
"file_name": "document.pdf",
"file_size": 102400
},
"file_urls": ["/api/files/uuid/document.pdf"],
"created_at": "2024-01-01T12:00:00Z"
}Behavior:
- If
record_idprovided: Attach file to existing record - If
record_idomitted: Auto-analyze file and create new record - CSV files: Create one record per row Errors:
400: Invalid file (too large, unsupported type)401: Missing or invalid bearer token413: File too large (>50MB)
Generate signed URL for file access.
Endpoint: GET /get_file_url
Query Parameters:
file_path: Path to file in storage (required)expires_in: URL expiration in seconds (optional, default: 3600) Request:
GET /get_file_url?file_path=files/uuid/document.pdf&expires_in=7200
Authorization: Bearer <token>Response: 200 OK
{
"url": "/api/files/uuid/document.pdf?token=...",
"expires_at": "2024-01-01T14:00:00Z"
}Errors:
400: Invalid request (missingfile_path)404: File not found401: Missing or invalid bearer token
Check if server is running.
Endpoint: GET /health
Request:
GET /healthResponse: 200 OK
{
"status": "ok",
"timestamp": "2024-01-01T12:00:00Z"
}No authentication required.
Get aggregate usage statistics for the authenticated user. Powers the Neotoma Inspector dashboard summary tiles. Figures are computed on each request from live data — no server-side cache.
Endpoint: GET /usage
Authentication: Required — Bearer token, AAuth grant, or MCP OAuth token. Guest principals are rejected with 401.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
user_id |
UUID string | No | Must match authenticated user if supplied. AAuth-admitted agents cannot override this. |
Request:
GET /usage HTTP/1.1
Authorization: Bearer <NEOTOMA_BEARER_TOKEN>curl example:
curl -s \
-H "Authorization: Bearer $NEOTOMA_BEARER_TOKEN" \
http://localhost:3080/usage | jq .Response: 200 OK
{
"entities_by_type": {
"conversation": 214,
"task": 156,
"contact": 88
},
"total_entities": 500,
"observations_by_source": {
"human": 310,
"llm_summary": 140,
"sensor": 50
},
"total_observations": 500,
"entities_created_last_7_days": 34,
"entities_created_last_30_days": 112,
"entity_types_with_schema": 6,
"entity_types_total": 10,
"last_updated": "2026-06-19T09:00:00.000Z"
}Response fields:
| Field | Type | Description |
|---|---|---|
entities_by_type |
Record<string, number> |
Active entity counts per type, sorted by count descending. |
total_entities |
number |
Total active (non-merged) entities. |
observations_by_source |
Record<string, number> |
Observation counts per observation_source; unsourced rows appear as "unclassified". |
total_observations |
number |
Total observations. |
entities_created_last_7_days |
number |
Entities created within the last 7 days. |
entities_created_last_30_days |
number |
Entities created within the last 30 days. |
entity_types_with_schema |
number |
Distinct entity types present in the data that also have an active registered schema. |
entity_types_total |
number |
Total distinct entity types present across all active entities. |
last_updated |
string (ISO-8601) |
Timestamp when these aggregates were computed (reflects request time, not a cache watermark). |
Error responses:
| Status | error_code |
Cause |
|---|---|---|
401 |
AUTH_REQUIRED |
Missing or invalid token; guest principal. |
403 |
FORBIDDEN |
user_id query param does not match authenticated user. |
500 |
DB_QUERY_FAILED |
Database query failure. |
OpenAPI: operationId: getUsage — see openapi.yaml and src/shared/openapi_types.ts (UsageStats schema).
CLI equivalent: neotoma request --operation getUsage
See also: docs/subsystems/usage_digest.md for the full endpoint reference including the distinction between /usage HTTP aggregates and usage_digest telemetry entities.
Get OpenAPI spec for API documentation.
Endpoint: GET /openapi.yaml
Request:
GET /openapi.yamlResponse: 200 OK
openapi: 3.0.0
info:
title: Neotoma API
version: 1.0.0
...No authentication required.
All errors follow this format:
{
"error_code": "INGESTION_FILE_TOO_LARGE",
"message": "File exceeds maximum size of 50MB",
"details": {
"file_size": 52428800,
"max_size": 52428800
},
"trace_id": "trace-uuid",
"timestamp": "2024-01-01T12:00:00Z"
}HTTP Status Codes:
200: Success201: Created400: Bad Request (client error)401: Unauthorized (missing/invalid token)403: Forbidden (insufficient permissions)404: Not Found409: Conflict (duplicate record)413: Payload Too Large429: Too Many Requests (rate limited)500: Internal Server Error503: Service Unavailable Seedocs/reference/error_codes.mdfor complete error code reference.
Current Limits (MVP):
- No explicit rate limiting (post-MVP feature)
- Recommended: <100 requests/second per client Future Limits:
- Per-user rate limits
- Per-endpoint rate limits
- Rate limit headers in responses
Use limit and offset for pagination:
{
"limit": 20,
"offset": 0
}Best Practices:
- Use
limit≤ 100 for optimal performance - Use
include_total_count: truefor UI pagination - Implement cursor-based pagination (post-MVP)
Load when:
- Implementing frontend API calls
- Integrating external clients
- Debugging API issues
- Understanding request/response formats
docs/specs/MCP_SPEC.md— MCP tool equivalentsdocs/reference/error_codes.md— Error code referencedocs/subsystems/errors.md— Error handling patterns
- Always include Authorization header — Except
/healthand/openapi.yaml - Validate request format — Check required fields
- Handle errors gracefully — Check error_code, not just HTTP status
- Respect rate limits — Implement exponential backoff
- Use pagination — Don't fetch all records at once
- Calling endpoints without authentication (except health/openapi)
- Ignoring error responses
- Fetching unlimited records (use pagination)
- Hardcoding bearer tokens in frontend code
- Bypassing rate limits