Base URL: http://localhost:3001 (dev). Interactive reference (Scalar) at
/docs; machine-readable /openapi.json
generated from the shared zod contracts.
Session cookie — Better Auth session (dashboard). The active project is selected
via the x-memoturn-project header (defaults to the user’s first project).
Write endpoints require a non-VIEWER role (viewers get 403).
Batched events (trace-create, span/generation-create/update, event-create, score-create). Returns 207 with per-event results: schema-invalid events are rejected individually in errors (id, index, reason) while valid events are accepted — inspect errors to catch silent data loss. Per-event input/output/metadata JSON capped at 1 MB (400 on oversize). Returns 429 when the per-project event rate limit (INGEST_EVENTS_PER_MINUTE) is exceeded; Retry-After header indicates when to retry.
Which auth methods are enabled (public, unauthenticated) — password/social/magic-link/email-OTP flags the console reads to render the sign-in surfaces the server accepts.
GET
/metrics
In-process API metrics (request counts, status classes, per-route latency percentiles, in-flight). Token-gated: returns 404 unless API_METRICS_TOKEN is set, then requires Authorization: Bearer <token>.
Distinct filter facet values + counts (environment, name, tags, scores, levels) over the range; params: days, limit, plus active filters (environment, search, userId, tag, scoreName, level) for facet-excluding counts.
GET
/v1/traces/histogram
Trace volume { interval, buckets } bucketed by hour (ranges ≤ 2 days) or day; honors the trace-list filters (environment, search, userId, tag, scoreName, level, days).
POST
/v1/traces/batch
Bulk action on selected traces: delete, add-to-dataset, or review.
GET
/v1/traces/{id}
Assembled trace: observations + scores.
GET
/v1/traces/{id}/similar
Traces semantically similar to this one (cosine over stored embeddings), most-similar first. Params: limit (≤ 50), days. Returns { data } of trace summaries + similarity.
POST
/v1/traces/{id}/replay
Re-run a stored trace’s input through the LLM gateway and record the result as a new trace. Body: { provider?, model? }. Audited.
POST
/v1/traces/{id}/annotate
Add a manual ANNOTATION score to a trace. Body: { name, dataType, value?, stringValue?, comment? }. Audited.
Paginated sessions { data, total } (traces grouped by sessionId); paging: page, pageSize (or legacy limit); scoped by days; search filters by sessionId substring.
GET
/v1/users
Paginated end users { data, total } (traces grouped by userId); paging: page, pageSize (or legacy limit); scoped by days; search filters by userId substring.
GET
/v1/metrics
Cost/token/latency rollups by day and model (days query).
GET
/v1/metrics/tools
Per-tool analytics — call volume, error rate, and p50/p95/avg latency by tool name (named SPAN observations) over days. The top agent-debugging view.
GET
/v1/metrics/cost-breakdown
Top spenders: cost rolled up by end user, session, or prompt, ranked by spend. Query: by (user|session|prompt, default user), days, limit.
GET
/v1/usage
Volume-based usage metering — bytes / events / traces ingested per UTC day, measured on the raw batch before sampling (the GB-ingested billing signal). Returns { total_bytes, total_events, total_traces, byDay }. Query: days (1–365, default 30).
POST
/v1/metrics/query
Run a dashboard/widget analytics query (view × metrics × dimensions × time × filters) from a JSON body; returns result rows. Read-only.
Compare a dataset’s runs side by side (per-item output + scores). Optional version scopes to runs of one dataset version.
POST
/v1/datasets/{name}/items
Append items.
POST
/v1/datasets/{name}/runs
Record an experiment run (link items → traces). Optional version pins the run to a dataset version (defaults to current).
POST
/v1/datasets/{name}/runs/{runId}/gate
CI quality gate: aggregate a run’s scores and check them against thresholds ({ scoreName: { min?, max?, maxRegression? } }; optional baselineRun for regression). Returns { passed, failures[], scores[] } for a CI exit code. Read-only.
GET
/v1/datasets/{name}/versions
List a dataset’s immutable version snapshots.
POST
/v1/datasets/{name}/versions
Cut a new version (freeze the current items). Body: { label?, description? }. Audited.
GET
/v1/datasets/{name}/versions/{version}
A version’s frozen items.
GET
/v1/datasets/{name}/export
JSONL download. format=items (backup dump) or format=oai-chat (OpenAI fine-tuning chat lines — items without expectedOutput are skipped; count in X-Memoturn-Skipped). Optional version=N exports a frozen version. Offloaded payloads are rehydrated.
One-shot completion. trace:true (default) records it as a trace.
POST
/v1/assistant/chat
In-app assistant: a bounded agentic loop over the project’s read-only MCP tools. Body: provider, model, messages[], optional context (organization/project/page/rangeDays); returns {content, steps[]}. Read-only.
POST
/v1/assistant/stream
Streaming assistant (SSE): same loop and body as /chat, but tool steps are emitted as they execute and answer text arrives incrementally (data: {"step":...} | {"delta":...} then [DONE]). Read-only.
POST
/v1/playground/stream
Streaming completion (SSE: data: {"delta":...} then [DONE]).
The prebuilt evaluator library (RAG/quality judge templates).
POST
/v1/evaluators/from-template
Instantiate a template into a project evaluator. Body: { key, name?, provider?, model?, ... }. Audited.
GET
/v1/evaluators/{name}/versions
Immutable judge-config version history (newest first). A version bumps when the prompt/model/provider changes, so online score drift is attributable to a config change.
POST
/v1/evaluators/{name}/run
Run over a trace’s input/output → writes an EVAL score.
Server-executed experiments run a prompt/model across a dataset and auto-score each item (a BullMQ job on the worker); results surface through the dataset comparison grid.
Webhook and automation target URLs are SSRF-validated on write: private IP ranges, loopback addresses, and cloud metadata endpoints are rejected with 400 (override with ALLOW_PRIVATE_WEBHOOK_TARGETS=1 for dev/LAN). The same check runs again at dispatch time to guard against DNS rebinding. Webhook deliveries carry X-Memoturn-Signature: sha256=<hmac> (HMAC-SHA256 of timestamp.body using the webhook secret) and X-Memoturn-Timestamp; the secret is returned once on creation and never again.
Method
Path
Description
GET / POST
/v1/webhooks
List (includes lastStatus/lastError/lastAttemptAt/failureCount delivery tracking) / create a webhook (POSTs on an event; score.created supports a low-score threshold). secret returned once on 201.
List / create a trigger→action automation (trigger: score.created/trace.created/eval.completed; action: webhook/slack). Target URL is SSRF-validated.
DELETE
/v1/automations/{id}
Delete an automation.
GET / POST
/v1/alerts
List / create a stateful alert rule. A worker cron evaluates metric (error_rate/latency_p95/cost_per_day/ingest_volume/dlq_depth) over a trailing window (minutes) against threshold per comparator (gt/gte/lt/lte), notifying channels ([{ type, target }]; type = slack/webhook (URL, SSRF-validated), pagerduty (Events-API routing key; auto-resolves), or email (address; needs an email transport configured)) once on firing and once on resolve.
Get / set / remove the project’s monthly cost budget (monthlyUsd + thresholds steps, default 50/80/100%). Notifies channels as month-to-date spend crosses each step. Soft only — no hard caps.
Multimodal attachments (images, audio, files). Inline base64 data URIs in trace/observation input/output are offloaded to blob storage at ingest time and replaced with a memoturn-media://<key> reference, so large payloads never bloat Doris; the console fetches them back through the GET route. Both routes require auth and are project-scoped.
Method
Path
Description
POST
/v1/media
Store a base64 data URI ({ "dataUri": "data:<mime>;base64,…" }). Returns 201 with { key, mimeType, url }.
GET
/v1/media/{key}
Fetch raw bytes back with the stored content-type (immutable, long-cache). 404 if the key isn’t in the caller’s project.
GET
/v1/payloads/{key}
Fetch a large input/output payload that was offloaded to blob at ingest (> 256 KB). The trace stores a { "_truncated": true, "ref": "memoturn-blob://<key>", "preview": … } marker; this returns the full serialized value. Project-scoped — 404 if the key isn’t payloads/<projectId>/….
Create a project in the caller’s active organization. OWNER/ADMIN only; audited.
PATCH
/v1/projects/{id}
Rename a project. {id} must be the active project. OWNER/ADMIN only; audited.
DELETE
/v1/projects/{id}
Delete a project and its data (relational rows cascade; telemetry purged best-effort). The last project in an organization can’t be deleted. {id} must be the active project. OWNER/ADMIN only; audited on the organization.
GET
/v1/projects/{id}/members
Project-level RBAC: the project’s org members, each with any per-project role override. {id} must be the active project.
PUT
/v1/projects/{id}/members/{userId}
Assign/update a user’s role on this project (overrides their org role). OWNER/ADMIN only; audited.
DELETE
/v1/projects/{id}/members/{userId}
Remove a user’s per-project role override (revert to org role). OWNER/ADMIN only; audited.
GET
/v1/audit-logs
Recent audit entries.
GET / POST
/v1/retention
Get / set retention (days; 0 = keep forever).
POST
/v1/retention/apply
Apply retention now.
GET / POST
/v1/sampling
Get / set ingest sampling. Head: rate (0–100 = percent of traces kept in the query store; 100 = all, stable per-trace). Tail keep-rules (kept regardless of the head dice below 100): keepOnError, keepLatencyMs, keepMinCostUsd (null = off). Dropped traces stay in blob for replay. Audited.
GET / POST
/v1/model-prices
List / create-update custom model price overrides (matched by name pattern, override built-ins).
DELETE
/v1/model-prices/{id}
Delete a model price override.
GET
/v1/exports/traces
Download traces as NDJSON (application/x-ndjson, default), CSV (?format=csv), or Parquet (?format=parquet, flat one-row-per-trace for BI); honors the trace-list filters: limit, environment, search, userId, tag, scoreName, level, days.
GET / POST
/v1/scheduled-exports
Get / configure the recurring daily NDJSON export of traces to blob storage.
POST
/v1/scheduled-exports/run
Run the export now and write the NDJSON to blob storage.
GET / POST
/v1/masking
Get / configure the PII redaction policy (built-in + custom patterns) applied to trace input/output at ingest.
POST
/v1/guardrails/check
Runtime guardrails: scan { text } for PII / prompt injection / SQL injection / blocked terms / required-match / JSON shape, plus opt-in evaluator (LLM-judge) guards; returns { verdict: allow|redact|block, findings, redactedText? }. Read-only compute (the evaluator-guard LLM calls write nothing and fail open on timeout/error). SDK: checkGuardrails / check_guardrails.
GET / POST
/v1/guardrails
Get / configure the project’s guardrail policy (PII action, prompt-injection/SQL-injection detection, blocked terms, requireMatch, requireValidJson/requiredJsonKeys, evaluator-backed evaluatorGuards).
GET / POST
/v1/analytics-sink
Get / configure the event sink — forwarding trace/score events to a product-analytics/CDP endpoint (PostHog-compatible capture API). POST host URL is SSRF-validated (400 on private/loopback targets).
GET / POST
/v1/api-keys
List project API keys (public key + hint) / mint a new pair (secret returned once).
DELETE
/v1/api-keys/{id}
Revoke an API key.
GET
/v1/account/mcp-connections
List the OAuth clients (remote MCP IDEs/agents) the signed-in user has authorized. Empty for API-key callers (no user).
DELETE
/v1/account/mcp-connections/{consentId}
Disconnect an OAuth client: deletes the consent and revokes its refresh tokens (access ends when the last ≤1 h JWT expires).
A remote Model Context Protocol endpoint exposing the project’s prompts, datasets, and review queues as tools for agent IDEs — the same tool registry (packages/server/src/mcp-tools.ts) the local stdio server serves (see MCP), over Streamable HTTP. Each project is its own MCP resource, so clients connect per-project. RBAC is per-tool (not per-method — every call is a POST): a tool’s mutating flag maps to a read/write permission, and write tools are audited.
Two auth paths resolve to the same per-project authorization:
API-key Basic (pk-mt-…:sk-mt-…, self-host / headless) — the key must belong to the {projectId} in the URL; the tool’s permission is checked against the key’s read/write scope.
OAuth 2.1 bearer (memoturn cloud, IDE click-through) — the Better Auth @better-auth/oauth-provider plugin issues a JWT access token (authorization-code flow with mandatory PKCE S256, rotating refresh tokens, dynamic client registration per RFC 7591); the API verifies it statelessly (signature via /auth/jwks, issuer, audience) and resolves its sub to a user, who is then authorized against {projectId} (org membership → role). Any member may run read tools; only non-VIEWER roles may run write tools. Clients discover the flow via the .well-known documents below; an unauthenticated request returns 401 with WWW-Authenticate: Bearer resource_metadata="…".
Method
Path
Description
GET / POST / DELETE
/v1/mcp/{projectId}
Streamable-HTTP MCP endpoint scoped to {projectId}. 401 (advertising Bearer + Basic) when auth is missing/invalid or the caller isn’t authorized for the project.
OAuth protected-resource metadata (RFC 9728) — advertises the API origin as the canonical resource for all per-project MCP URLs.
Behind Caddy (single-VM prod), the three .well-known paths are routed to the API (they’re served at the domain root, not the console) — see infra/Caddyfile. The OAuth authorize flow bounces unauthenticated users to the console sign-in page (MCP_LOGIN_PAGE, default <first AUTH_TRUSTED_ORIGINS>/login) and scope approval to the console consent page (MCP_CONSENT_PAGE, default <first AUTH_TRUSTED_ORIGINS>/consent).