Overview
Oreag turns your documents into a queryable RAG API - and an interlinked agent-memory "brain" - in minutes. Upload your files, tune how they are chunked and embedded, bring your own model-provider keys (or run a local model), and Oreag gives you a grounded /v1 REST API and an MCP connector your apps and coding agents can call.
Oreag is delivered as:
- A web dashboard (Next.js) where you create projects, upload documents, watch indexing progress, test queries in a Playground, and manage keys.
- A REST API with two surfaces - an owner/dashboard surface (
/api/..., authenticated with your Supabase session) and a public, per-project surface (/v1/..., authenticated with a project API key shaped likeoreag_sk_…). - An MCP server (
oreag-mcp) that gives coding agents (Claude Code, Codex, claude.ai / Claude Desktop) per-project memory plus RAG over a project's documents.

About the screenshots: New UI examples use sample projects and illustrative metrics.
What makes Oreag different: the "brain"
In Oreag, a project's document chunks and its agent memories live in the same per-project embedding space - the same provider, model, and vector dimension. Because they share one space, a single cosine-similarity operator compares them directly. This single design choice underpins three capabilities:
| Capability | Endpoint | What it does |
|---|---|---|
| Grounded chat | POST /v1/projects/<project-id>/query | Retrieval-augmented answers that blend in relevant memories alongside document chunks as context. |
| Agentic exploration | POST /v1/projects/<project-id>/explore | Seeds on the nearest chunks and memories, then walks related links outward (0–3 hops) to return a connected subgraph. |
| Static memory graph | GET /v1/projects/<project-id>/memory-graph | The full graph of project → files → sections → chunks plus memories, with automatic cross-file and memory-relatedness edges. |
What you can do with Oreag
- Build a RAG API over your own files. Upload any file that text can be extracted from: rich formats (PDF, DOCX, PPTX, XLSX/XLS, HTML, RTF, EPUB, ODT/ODS/ODP, EML, ZIP) are converted with MarkItDown, images are AI-captioned with your answer model, audio is transcribed with your own provider keys, and everything else - code, configs, logs, plain text - ingests as-is. Each file is converted to Markdown, split into chunks, and embedded.
- Tune retrieval to your data. Control
chunk_size,chunk_overlap, the embedding model, the answer (LLM) model, andtop_k. - Bring your own keys (BYOK). Use your own OpenAI, Google Gemini, Anthropic, xAI (Grok), Groq, Mistral, DeepSeek, Cohere, or Sarvam keys at the account level, and optionally override them per project. Or skip keys entirely with local providers (Ollama, LM Studio, sentence-transformers).
- Give agents memory. Through the MCP server, connected agents can save decisions and facts, recall them later, and explore how knowledge and memory connect.
Health and availability
The backend exposes an unauthenticated health probe:
curl https://oreag.onrender.com/healthz
# {"status":"ok"}The MCP server exposes its own unauthenticated probe at GET /health → 200 ok.
The rest of this guide covers the core concepts behind a project, then a step-by-step getting-started walkthrough from sign-up to your first live API call.
Core Concepts
This section defines the building blocks you will work with in Oreag: projects, documents and chunks, embeddings, agent memory, the combined brain, and the two kinds of provider keys versus project API keys.
Projects
A project is the top-level container that owns everything else - its files, chunks, API keys, memories, and query logs. Each project belongs to one account (its owner_id) and carries its own configuration:
| Field | Default | Notes |
|---|---|---|
name | - | Unique per account (case-insensitive); up to 200 chars at the API (the wizard caps the input at 20). |
description | - | Optional. |
chunk_size | 1000 | Characters per chunk. Valid range 100–8000. |
chunk_overlap | 200 | Must be >= 0 and strictly less than chunk_size. |
embedding_provider / embedding_model | openai / text-embedding-3-small | Defines the embedding space; changing it re-indexes the project. |
embedding_dimensions | 1536 | The model's default; Matryoshka models (text-embedding-3-*, gemini-embedding-001) also accept smaller prefix sizes. |
llm_provider / llm_model | openai / gpt-4o-mini | The answer model. |
top_k | 5 | Default number of chunks retrieved. Valid range 1–20. |
status | empty | One of empty, indexing, ready, error. |
A project's status is recomputed automatically: empty (no files), indexing (files pending/processing), error (a file failed), or ready.
Safe instant edits (name, description, top_k, LLM provider/model, key overrides) apply immediately via PATCH /api/projects/<project-id>. Chunking and embedding-model changes are not instant - they re-process every file and go through the re-index flow.
Documents → chunks
When you upload a file, Oreag runs it through ingestion:
- Convert the file to Markdown (via MarkItDown).
- Split the Markdown into overlapping chunks using your
chunk_size/chunk_overlap(per-file overrides fall back to the project defaults). - Embed each chunk into a vector and store it.
- Mark the file
indexedand record itschunk_count.
Each file moves through statuses pending → processing → indexed (or failed, with an error / conversion_error message). PDFs also record a page_count. Per-file limits: 50 MB maximum per file, and each file must contain extractable text.
Each chunk stores its content, its chunk_index, an optional page_number, and its embedding vector. Chunks are the document half of the brain.

Embeddings and the embedding space
An embedding is a vector that represents the meaning of a piece of text. Oreag embeds both document chunks and agent memories with the same project-wide embedding model, so every vector in a project has the same dimension and is directly comparable with cosine similarity (the pgvector <=> operator).
Retrieval is hybrid: alongside the semantic (vector) search, a Postgres full-text pass catches exact terms - error codes, part numbers, names - that embeddings can miss, and the two rankings are fused with Reciprocal Rank Fusion. Retrieval only runs when a question misses both answer caches (exact-match and semantic); cached questions never touch the chunks table.
Because the embedding model defines this shared space, changing the embedding model (or chunking) re-processes every file - Oreag wipes all chunks and re-queues every file, and memory embeddings are re-embedded with the new model. Changing only a key is instant. One more exception: shrinking the same Matryoshka model to a smaller dimension size is applied instantly by truncating the stored vectors in place - no re-embedding at all.
Agent memory
Memories are short notes - decisions, facts, context - that your connected agents save (typically via the MCP server). Each memory has:
content(1–8000 chars), optionaltags, apinnedflag, and asource(defaults to"mcp").- An embedding that is nullable: if no embedding key is available when the memory is saved, it is stored without an embedding and is not searchable until re-embedded.
Memories are the memory half of the brain. They are managed through the public MCP-facing endpoints (/v1/.../memory*) and viewed/deleted in the project's Memory tab.

The brain = chunks + memories in one space
Because chunks and memories share one embedding space, Oreag can treat them as a single connected graph - the brain. Three features build on this:
/queryblends the most relevant memories into the RAG context alongside document chunks (see blending below)./exploreseeds on nearest chunks and memories, then expands alongrelatedlinks for a number ofhops(0–3), returning a subgraph of nodes and edges./memory-graphreturns the full static graph: the project, its files, Markdown sections, chunks, and memories, with structural edges (contains,derived_from,next) and semanticrelatededges (cross-file chunk similarity and memory relatedness).
Memory blending in /query: when answering, Oreag searches a small number of memories and appends those above a similarity threshold as pseudo-sources (shown with filename="memory"). If no embedding key is available, blending is silently skipped and the answer uses document chunks only.

Two kinds of keys - don't mix them up
Oreag has three distinct key concepts. The first two are keys Oreag uses to call AI providers on your behalf; the third is a key other programs use to call Oreag. They are never interchangeable.
| # | Concept | What it is | Where to manage it | Looks like |
|---|---|---|---|---|
| 1 | Account-level provider keys | Your OpenAI / Gemini / Anthropic / Sarvam key. One per provider, shared by every project. | Sidebar → API keys → Provider API keys card | sk-…, shown as ••••••••<last4> |
| 2 | Project-level key overrides | One project choosing its own provider key for its embedding and/or answer model. | Project → Settings tab → key field under Answer model / Indexing & embedding | Project key ••••<last4> |
| 3 | Project API keys | Bearer tokens external apps / agents / MCP clients use to call this project's /v1 + MCP endpoints. | Project → API tab → Create key | oreag_sk_…, shown once at creation |
Key resolution order (concepts 1 and 2): a project's per-project override wins; otherwise the account-level provider key is used; otherwise none. Local providers (ollama, sentence_transformers) need no key at all.
Supported account-level providers and what each is used for:
- OpenAI - embeddings + chat (
sk-…) - Google Gemini - embeddings + chat; accepts both AI Studio keys (
AIza…) and Vertex AI express-mode keys (AQ.…) - Oreag routes each to the right Google backend automatically - Anthropic (Claude) - chat only
- Azure OpenAI - embeddings + chat; enter your resource endpoint alongside the key, and name deployments after their model (e.g. a gpt-4o deployment called "gpt-4o")
- Mistral - embeddings + chat
- Cohere - embeddings + chat (embed-v4.0 supports Matryoshka sizes)
- Together AI - embeddings + chat (open models)
- Fireworks AI - embeddings + chat (open models)
- xAI (Grok) - chat only
- Groq - chat only (fast open models)
- DeepSeek - chat only
- OpenRouter - chat only (one key, many upstream models)
- Perplexity - chat only (Sonar)
- Voyage AI - embeddings only
- Jina AI - embeddings only (jina-embeddings-v3 supports Matryoshka sizes)
- Sarvam AI - chat only (Indic LLMs)
Keyless local providers: Ollama, LM Studio (both probed for availability), and sentence-transformers (in-process)
The account API keys page shows both the Provider API keys card and a read-only Project key overrides card together, so you can see at a glance which projects use their own keys.

Mental model: concepts 1 and 2 let Oreag talk to the AI providers. Concept 3 lets your software talk to Oreag. If you are wiring up an app, a script, or an MCP client, you want a project API key (concept 3).
Getting Started
This walkthrough takes you from creating an account to calling your project's live RAG API. The flow is: sign up → create a project → upload documents → ask a question in the Playground → create an API key → call the endpoint.
1. Sign up
- Open the Oreag landing page and click Get started (or Sign in if you already have an account).
- On the Sign Up screen, enter your email and a password. Passwords must satisfy the live rules: at least 12 characters, one uppercase letter, and one special character. Unmet rules are listed inline as you go.
- Submit. Depending on your instance:
- If email confirmation is disabled, you go straight to the dashboard.
- If it is enabled, you'll see "Check your inbox - we sent a confirmation link"; click the link to finish.
If you try to sign up with an email that already has an account, Oreag tells you so and links you to Sign in.

2. Create a project
From the dashboard, click New project. The wizard has two steps.
Step 1 - Name and documents
- Enter a project name (the input is capped at 20 characters with a live counter; duplicate names are flagged).
- Optionally add a description.
- Drag and drop (or click to browse) the documents you want to index. Each file must be under 50 MB. Any file with extractable text is accepted - rich formats are converted with MarkItDown (list below), everything else ingests as plain text; only opaque binary is rejected:
.pdf .docx .pptx .xlsx .xls .html .htm .csv .json .xml .txt .md .rtf .odt .ods .odp .epub .eml .jpg .jpeg .png .gif .bmp .tif .tiff .wav .mp3 .m4a .zip
Click Configure to continue.

Step 2 - RAG configuration
- Chunk size (100–8000) and chunk overlap (≥ 0, less than chunk size).
- Embedding model - grouped by provider; only providers with a usable key (or local Ollama) are offered. This cannot be changed later without re-indexing.
- Answer model (LLM) - the chat model that writes answers.
- Top-K - how many chunks to retrieve (1–20).
If the selected provider lacks a usable key, an amber banner links you to Settings → API keys to add one (see Core Concepts → keys). Click Create (labeled "Create & index N file(s)" when files are attached). Oreag creates the project, uploads your files, and starts indexing.
Tip: You can also create a project entirely over the API. The owner endpoint is
POST /api/projects(authenticated with your dashboard session), and files are uploaded toPOST /api/projects/<project-id>/files.

3. Upload documents (and watch indexing)
If you didn't attach files in the wizard, open the project and use the Files tab → Add files. While files index, the project and file lists auto-refresh, and each file shows a status pill:
| File status | Meaning |
|---|---|
Queued (pending) | Waiting to be processed. |
Indexing (processing) | Being converted, chunked, and embedded. |
Indexed (indexed) | Ready to query. |
Needs review (failed) | Conversion or indexing failed - see the error text; use Retry. |
The project is ready to query once at least one file is Indexed (project status ready).

4. Ask a question in the Playground
Open the project's Playground tab - it runs the exact same pipeline your API consumers will use, but with your dashboard session (no API key needed, no key logging).
- Type a question in the composer (Enter to submit; Shift+Enter for a newline).
- Oreag retrieves the top chunks, blends in any relevant memories, and generates a grounded answer.
- The result shows the answer, the model and latency (e.g.
openai/gpt-4o-mini / 812 ms), and expandable References - each[i] filename - page N (X% match)reveals the source chunk.
You can also switch the answer model from the Playground; the change is saved to the project immediately.

5. Create a project API key
To call the project from outside Oreag, you need a project API key (concept 3 from Core Concepts).
- Open the project's API tab.
- Click Create key. A dialog shows the full key (
oreag_sk_…) once - copy it now; it is never shown again. - The key appears in the table as Active.
Read vs. read+write: each key row has an Uploads checkbox in the Access column. Leave it unchecked for a read-only key (query/retrieve only); check it to allow the key to upload files via /v1/.../files. Read-only keys that attempt an upload get a 403.
You can Revoke a key (it stops working immediately but stays in the table for audit) or Delete it (permanently purged) from the row's ⋯ menu.

6. Call the endpoint
All public calls use the /v1 surface and authenticate with Authorization: Bearer YOUR_API_KEY. The key is scoped to exactly one project, so the <project-id> in the URL must match.
Ask a grounded question - POST /v1/projects/<project-id>/query
Body: question (1–4000 chars) and optional top_k (1–20).
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/query" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "What is our refund policy?"}'Example response:
{
"answer": "Refunds are issued within 30 days of purchase [1]. ...",
"sources": [
{
"filename": "policies.pdf",
"page_number": 4,
"chunk_index": 12,
"content": "Customers may request a refund within 30 days...",
"similarity": 0.83
}
],
"model": "openai/gpt-4o-mini",
"latency_ms": 812
}The same call in JavaScript:
const res = await fetch(
"https://oreag.onrender.com/v1/projects/<project-id>/query",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ question: "What is our refund policy?" }),
}
);
const { answer, sources } = await res.json();
console.log(answer, sources);Retrieve passages only (no LLM) - POST /v1/projects/<project-id>/retrieve
Use this when you want raw matching chunks without a generated answer. Body: query and optional top_k (defaults to 5).
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/retrieve" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "refund window", "top_k": 3}'Upload a document over the API - POST /v1/projects/<project-id>/files
Requires a key with Uploads enabled (otherwise 403). Files use the project's default chunking and embedding.
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/files" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "uploads=@./handbook.pdf"Other public endpoints at a glance
| Method & path | Purpose |
|---|---|
GET /v1/projects/<project-id> | Lightweight project info (id, name, status, file_count). |
POST /v1/projects/<project-id>/explore | Agentic exploration over the brain (query, hops 0–3, default 1). |
POST /v1/projects/<project-id>/memory | Save an agent memory (content, tags, pinned). |
POST /v1/projects/<project-id>/memory/search | Cosine search over embedded memories. |
GET /v1/projects/<project-id>/memory/recent | Recent + pinned memories (limit, default 10). |
GET /v1/projects/<project-id>/memory-graph | Full memory graph (nodes + related edges). |
Public ingest limits
Uploads via /v1 are guarded (owner dashboard uploads are exempt):
| Limit | Value |
|---|---|
| Max files per upload request | 20 |
| Max files per project | 1000 |
| Upload rate per project | 60 per minute (sliding 60s) |
| Max size per file | 50 MB |
7. (Optional) Connect a coding agent via MCP
To give an agent like Claude Code per-project memory + RAG, add the Oreag MCP connector with your project's URL and key:
claude mcp add --transport http oreag \
https://<mcp-host>/projects/<project-id>/mcp \
--header "Authorization: Bearer YOUR_API_KEY"The agent can then save_memory, search_memory, list_recent_memory, search_docs, ask_docs, add_document, get_memory_graph, and explore_brain - all scoped to your project. A typical session bootstraps with list_recent_memory, works using search_docs / explore_brain, and records decisions with save_memory.
That's the full path from sign-up to a live, grounded API call. From here, explore the Memory tab to see what your agents remember, tune retrieval in Settings, or wire your own provider keys under Settings → API keys.

Signing in
Oreag offers three ways in. They are layered by strength: the strongest one that succeeds is the only step you need.

Passkeys (recommended)
A passkey signs you in with your face, fingerprint or device PIN. There is no password to type, remember or leak, and a passkey cannot be used on a lookalike site the way a typed code can - it is bound to the real domain by the browser.
Add one under Settings -> Profile -> Two-factor authentication -> Add passkey. Give it a name you'll recognise later ("Work laptop", "iPhone"), so you can revoke the right one if you lose a device. Passkeys sync through iCloud Keychain, Google Password Manager or 1Password, so they usually survive losing a single device.
A passkey sign-in is complete on its own. You will not be asked for a second code afterwards, because a passkey is already two factors: the device you hold, plus the biometric or PIN that unlocked it.
Password, or a 6-digit code
You can sign in with your password as usual, or choose Email me a code instead and type the 6-digit code we send. The code path is handy when you are on a phone, or if you signed up with Google or GitHub and never set a password.
Two-factor authentication
If you add an authenticator app (Google Authenticator, 1Password, Authy), signing in with a password or an emailed code will ask for a 6-digit code afterwards. Signing in with a passkey will not.
Recovery codes provide a way back in if you lose a device. Store them securely; the card shows how many are unused and lets you regenerate them. Also add two methods - for example a passkey and an authenticator app, or passkeys on two devices.

Verification codes
Every email we send for signup confirmation, password reset or reauthentication contains both a 6-digit code and a link. Use whichever suits you: type the code where you are, or click the link. The link works even if it opens in a different browser from the one you started in.
Codes expire quickly. If yours has expired, use Resend code - there is a short cooldown between sends.
Changing your password
Changing your password while signed in requires a code emailed to you first, even though you are already logged in. This is deliberate: it means that somebody who gets hold of an active session on an unlocked machine still cannot lock you out of your own account by silently changing the password.

Dashboard
The dashboard is your home base in Oreag. It lists every RAG project on your account, surfaces each project's live indexing status, and gives you a persistent sidebar for moving between projects, files, and account screens. This page is served at /dashboard.
The Projects overview
The main panel (/dashboard) is headed "Projects" with a New project button in the top-right that takes you to the new-project wizard (/projects/new). The list is fetched from GET /api/projects and shows your projects newest-first.

Load, empty, and error states
| State | What you see |
|---|---|
| Loading | Three skeleton placeholder cards |
| Error | An inline message: Could not load projects: {message} |
| Empty (no projects) | A centered card with a file icon, the text "No projects yet…", and a Create your first project button |
| Populated | A responsive grid of project cards (1 column on mobile, 2 on tablet, 3 on desktop) |
Behind the scenes the dashboard also preloads
/api/provider-keysso that opening Settings → API keys later is instant.
Project cards
Each card links to that project's detail page (/projects/<project-id>) and contains:
- Title (truncated if long).
- Status button - a glowing capsule whose spinning outline color reflects the project's current status (see the status table below).
- Description (optional), clamped to two lines.
- Animated stat ticker pinned to the bottom of the card: an infinite marquee showing
file_countfiles ·chunk_countchunks ·query_countqueries · the created date (in your locale). The numbers are rendered in a monospaced, tabular-figures font. The marquee pauses on hover and respects theprefers-reduced-motionsetting. Faded edges mask the loop seam. - Navigation spinner - when you click a card and navigation is pending, a spinner appears in the card corner.
The file_count, chunk_count, and query_count values are computed server-side and returned with each ProjectOut.
Status indicator colors
A project's status is one of four values, each with a consistent color across the whole UI (cards, sidebar dots, project header):
| Status | Color | Meaning |
|---|---|---|
empty | Gray | No files indexed yet |
indexing | Amber | Files are being chunked and embedded |
ready | Emerald / green | All files indexed and queryable |
error | Red | At least one file failed to index |
While a project is indexing, the dashboard and the project detail page poll automatically so the status updates without a manual refresh.
Live cache sync
When you open a project and return, the detail page pushes its fresh data back into the dashboard's /api/projects SWR cache, so the cards never show stale counts or status after you navigate back.
Sidebar navigation
The sidebar is a fixed 16rem left column on md and larger screens. It is the primary way you move around the app.

Header and main navigation
The sidebar header shows the Oreag brand mark and a "Oreag / RAG & Memory" label that links back to /dashboard. Below it, the main navigation has three items, each with an icon and active-state highlighting:
| Item | Route | Active rule |
|---|---|---|
| Overview | /dashboard | Exact match |
| New project | /projects/new | startsWith |
| API keys | /settings/api-keys | startsWith |
Context-switching list (Projects vs. Files)
Below the main nav is a context-aware list whose header label and contents change based on where you are. The header carries a count badge.
On the dashboard (and most routes) → Projects list.
- A "Search projects" box filters by name, description, or status.
- Each row shows a folder icon, the truncated project name, and a right-side status dot colored by the project's status (gray / amber / emerald / red). The dot swaps to a spinner while navigation to that project is pending.
- Empty states: "No projects yet" or "No matching projects". Loading shows two skeleton rows.
Inside a project (/projects/<id>) → Files list.
- A "Search files" box filters the file list.
- Files are sorted newest-first and grouped by file type (PDF, DOCX, …) into collapsible sections, each with a count badge. Sections auto-open while you're searching.
- Each file row shows a file-type icon and a name capped at 12 characters with an ellipsis (full name on hover via the
titleattribute). Clicking a file navigates to/projects/<id>?file=<file-id>; a brief loader animation plays first. Modified or middle clicks open normally. - Empty states: "No files yet" or "No matching files".
The list scrolls with a hidden scrollbar and a capped height.
Footer
A separator divides the list from the user menu at the bottom.
User menu
The user menu shows your avatar plus your display name (avatar-only in compact/mobile mode). It stays live across profile updates by re-reading the session on auth-state changes.
- Display name resolves to your
user_metadata.username, falling back to the local part of your email, then to "Account". - Avatar uses your uploaded
avatar_url, falling back to a Gravatar for your email, then to an initial letter. - The dropdown contains a label block (name + email), a Profile link →
/settings/profile, and a destructive Sign out action that signs you out and returns you to/login.
Mobile & responsive behavior
Below the md breakpoint the fixed sidebar is hidden and replaced by a sticky, blurred top bar:
- A hamburger (list) icon opens a left drawer (a
Sheet, 18rem wide) containing the full sidebar body. - The brand mark links to
/dashboard. - A compact user menu (avatar only) sits on the right.
- The drawer closes automatically on route change.
The project cards grid collapses from 3 → 2 → 1 columns as the viewport narrows. All status colors, spinners, and the stat marquee behave identically on mobile.
Shared navigation-pending state
Projects use a shared nav-pending store, so clicking a project from any surface (a dashboard card or a sidebar row) lights up the loading spinner on all surfaces for that project at once. This keeps the card spinner and the sidebar status-dot spinner in sync.
Usage analytics
The Usage page turns provider metering into an account-level operating view: demand, token volume, spend, cache savings, response time, retrieval quality, and allocation across API keys, models, and projects. Open it from the dashboard sidebar at Usage (/settings/usage).
Choose 7 days, 30 days, or 90 days at the top. Every summary card, chart, insight, and table updates to the same selected window.

The six top metrics answer the fastest account questions: total requests; prompt, completion, and embedding tokens; total measured cost; and cost avoided by cache hits. Cost avoided compares saved generation cost with the generation cost that would have been incurred without those savings.
Missing data is not zero. When a provider does not report a value, Oreag displays Not measured or leaves a gap in the chart. It does not silently convert an unknown value into zero or include a partial number in a complete total.
Jump to Budgets and alerts to set spending warnings.
Spending changes and estimates
What changed your spending? splits the change in recorded LLM and embedding spend into a request-volume effect and a cost-per-request effect. The 7-day view compares the last three complete UTC days with the previous three; the 30- and 90-day views compare two seven-day periods. Today and the rolling window's partial first day are excluded. Cost per request is the period's recorded spend divided by all its recorded requests, including ingestion and other operations.
The request-volume effect holds the previous cost per request constant. The remaining change is the cost-per-request effect; both add up to the spend change before display rounding. For example, 100 requests costing $10 followed by 200 requests costing $30 gives a $20 increase: $10 from the volume effect and $10 from the rate effect. Model prices, workload, tokens, and caching can all affect the blended rate. This arithmetic breakdown and the cache comparison do not establish what caused a change.
Next 30 days is explicitly an Estimate: recorded spend per complete day in the recent comparison period multiplied by 30. It assumes that spending rate continues, projects the next 30 days rather than the calendar month's total, and does not change budgets or request limits. Actual provider charges can differ. At least three complete days and recent recorded requests are required. Missing day buckets count as no recorded activity; an actual zero-cost request can support a zero estimate.
Unreported usage, missing prices, excluded vision/audio costs, or activity without recorded costs show Incomplete measurements and withhold both spending effects and the estimate. Coverage warnings apply to the selected window because measurement caveats are not available separately for each comparison period. Available figures remain labelled as recorded spend; unknown costs never become zero. Without previous requests, there is no cost-per-request baseline for the breakdown.
Where the money goes
The donut separates generation spend (prompts and model responses) from embedding spend (indexing and retrieval vectors). The percentages describe each category's share of measured spend. The cost-density figures normalize both categories to cost per one million tokens, which makes differently sized workloads comparable.
Example: if embeddings produced 79.4% of measured tokens but only 16.4% of spend, the account is processing far more embedding tokens while most cost still comes from generation.


Requests per day
Each bar is the number of metered requests on one day. Use the shape to identify traffic growth, quiet days, and sudden demand spikes. A bar of 120 means Oreag recorded 120 requests that day; it does not describe tokens or latency.

Tokens per day
This chart plots daily prompt, completion, embedding, and saved prompt token volume. Compare the lines to see whether cost changes are driven by larger prompts, longer answers, indexing/retrieval work, or cache savings.
Embedding volume can be much larger than generation volume while costing less per token. A missing point is an unmeasured provider value, not a zero-token day.

Response time percentiles
The filled area chart plots three daily latency percentiles:
| Series | Meaning | If the chart shows 18s |
|---|---|---|
| p50 (typical) | The median request. Half completed at or below this time and half took longer. | p50 = 18s means 50% of requests completed within 18 seconds and 50% took more than 18 seconds. |
| p95 (slow tail) | The boundary for the slowest 5% of requests. | p95 = 18s means 95% completed within 18 seconds; the slowest 5% took longer than 18 seconds. |
| p99 (worst tail) | The boundary for the slowest 1% of requests. | p99 = 18s means 99% completed within 18 seconds; the slowest 1% took longer than 18 seconds. |
A percentile is not an average. For example, p95 = 18s does not mean 95% of requests each took exactly 18 seconds. It means 18 seconds is the upper boundary for 95% of the observed requests. If p50 stays stable while p95 and p99 rise, typical users are unaffected but the slow tail is getting worse.

Traffic by endpoint
Horizontal bars rank API surfaces by request count. This answers which operations are creating account demand - for example query, retrieve, explore, or memory calls. Hovering a bar also shows its typical p50 response time when measured.
A high bar means an endpoint is used often; it does not by itself mean the endpoint is expensive or slow. Compare it with cost and latency before drawing that conclusion.

Cache composition
Each daily stacked bar divides answered queries into L1 exact hits, L2 semantic hits, and misses.
- L1 exact: the same normalized question was answered from the exact cache.
- L2 semantic: a sufficiently similar cached question supplied the answer.
- Miss: the request continued to retrieval and model generation.
The displayed cache-hit percentage is (L1 + L2) / (L1 + L2 + misses). A 60% hit rate means 60 of every 100 cacheable answered queries were served without a new model generation call.

Tokens by model
Generation models and embedding models use separate horizontal charts and separate scales. Embedding workloads can be orders of magnitude larger, so putting every model on one shared axis would make generation bars nearly invisible.
Read bar length only against other models in the same chart. Hover for the exact token count, request count, and measured model cost. Do not compare the physical length of a generation bar directly with an embedding bar because their axes are intentionally independent.

Retrieval quality
The area chart shows the daily mean similarity of the chunks returned by retrieval on a fixed 0.0 to 1.0 scale. Higher values mean retrieved chunks are, on average, closer in embedding space to the questions asked.
Example: 0.78 means the returned chunks had an average similarity score of 0.78. It does not mean the answer was 78% correct. Treat similarity as a directional retrieval signal: a sustained fall can indicate that the indexed content is drifting away from user questions, while a single-day movement may simply reflect a different query mix.
Oreag needs at least three measured days before drawing a trend. Missing days remain gaps.

Spend by project
The project portfolio ranks the highest-volume projects by measured provider cost. The tooltip adds request count and average cost per request, allowing you to distinguish a busy low-cost project from a smaller but expensive workload.
Example: $22.80 · 612 requests · $0.04/request means the project accumulated $22.80 of measured cost across 612 requests, averaging roughly four cents per request in the selected window.

Project quality and cache efficiency
This grouped horizontal chart compares three project-level signals on the same 0% to 100% visual scale:
- Cache hit rate: the share of cacheable requests answered from L1 or L2.
- Retrieval similarity: the project's average retrieved-chunk similarity.
- Cache similarity: the average similarity for semantic-cache matches.
Similarity values are displayed as percentages for scale readability - for example raw 0.81 appears as 81% - but they are similarity scores, not correctness percentages. If a project has no measured similarity, its bar is absent rather than drawn at zero.
Detailed tables
Use View daily data as a table when you need exact daily values behind the charts. The By API key, By model, and By project tables show the first three rows initially; choose View all to reveal the remaining rows inside the same fixed-height, scrollable card. These tables are the detailed audit view, while the charts are optimized for comparison and trend detection.




Budgets and alerts
Open Usage → Budgets & alerts. Budgets live on the Usage page, and the unread alert count appears beside its existing sidebar link.
Set up a budget
- Select Add budget.
- Choose Entire account or a project under Budget scope.
- Enter the Monthly budget (USD), such as $100 for your account.
- Set Early warning (%). The default is 80%, so a $100 budget warns at $80. A second alert is saved at $100.
- Leave Enable alerts checked and select Save budget.

You can save one budget per scope, up to 26 in total. On mobile, use the same Add budget button and select the project in the dialog.

Read and manage warnings
The budget card shows recorded spending, the monthly amount and a progress bar. Near budget marks the early-warning threshold; Budget reached marks 100% or more. Open Alerts to see the saved warnings. Choose Mark read for one warning or Mark all read to clear the unread count, including older warnings.
Use the pencil icon to change the amount or warning percentage. Turn off Enable alerts and save to pause new warnings; the card shows Alerts paused and retains the history. The trash icon opens a confirmation to remove that budget and its alerts. Usage records remain available.
The screenshots use illustrative spending and project names.
Budgets only warn. Reaching a threshold never suspends a project, blocks an API request, changes project settings, or stops Playground or evaluation runs. Account and project budgets overlap; project spend is already included in the account amount.
Budgets use the UTC calendar month, resetting on the first, independently of the Usage page’s 7/30/90-day selector. They sum recorded generation and embedding costs across API, Playground, ingestion, memory and evaluation events. Cache savings are excluded. No events means zero recorded spend; events with no measured cost show Not measured. Missing prices or usage can make totals incomplete, so this is a recorded-spend warning, not a provider invoice or a guaranteed spending cap.

A backend worker checks enabled budgets about once a minute while the backend is running, even when your dashboard is closed. Open Alerts for recent warnings; the existing Usage sidebar link shows an unread count. Marking an alert read persists across sessions. Use Mark all read to clear the unread count, including older alerts beyond the 50 shown. Alerts are delivered inside Oreag, without email or push notifications.
Each percentage generates at most one alert per budget per month. If spend jumps past both thresholds, both alerts are saved. An alert retains the budget amount and spend at detection time; editing the budget does not rewrite past alerts. Pausing stops new alerts but keeps history. Resuming checks the current month immediately. After downtime, checks catch up within retained metering history (90 days by default). The UI and unread count refresh once a minute while visible; detection and display can each take a refresh cycle.
Budget endpoints
These endpoints require the signed-in owner’s dashboard bearer token, with the existing MFA and account-suspension checks. Project API keys cannot manage account budgets. No changes are needed in your public API integration: its metered costs count automatically.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/account/budgets | Current UTC month, budgets and recorded spend, up to 50 recent alerts, and the total unread count. |
| PUT | /api/account/budgets | Create/update one scope with {project_id, amount_usd, warning_percent, enabled, revision}. Use null project_id for the account, revision 0 for a new scope, or the returned revision for an edit. Returns the updated report. |
| DELETE | /api/account/budgets/{budget_id} | Remove an owned budget and its alerts; metering records are preserved. Returns 204. |
| GET | /api/account/budgets/alerts | Recent alerts and unread count, without recalculating spend. |
| POST | /api/account/budgets/alerts/{alert_id}/read | Mark an owned alert read, idempotently. Returns 204. |
| POST | /api/account/budgets/alerts/read-all | Mark all of this owner's alerts read, including older ones. Returns 204. |
Amounts must be greater than zero, at most $1,000,000, and have at most two decimal places. An outdated revision returns 409; inaccessible projects, budgets and alerts return 404. Apply migration 0045_usage_budgets.sql before deploying the backend and frontend changes.
Operations
Expand Operations in Usage for failed/disconnected requests, separate 4xx rejections, p95 response latency, p95 first-token timing, queue counts/age and worker availability. Health displays a compact notice when attention is needed. Measurements cover authenticated API and project routes and arrive asynchronously. They use at most the latest 5,000 requests within 24 hours; unmeasured values remain unreported. This diagnostic sample is separate from billing usage.
Warnings inspect the latest 15 minutes: at least 20 requests with 5% failures/disconnects or p95 above 10 seconds, jobs active/queued over five minutes, unavailable workers, or shared database pressure. The bounded in-memory telemetry buffer can lose events on process termination or overload. Queue age includes processing time. No request bodies, questions or credentials are recorded in these metrics.
GET /api/account/operations is owner-session only. Deployment readiness is available separately at GET/HEAD /readyz (200 ready, 503 not ready), using worker liveness and recent database writes. /healthz remains the process liveness endpoint. External provider/storage health is not inferred from readiness.


Projects
A project is a self-contained RAG knowledge base: its own documents, chunking and embedding configuration, answer (LLM) model, agent memories, and API keys. This section covers creating a project with the two-step wizard and gives an overview of the six tabs you use to manage it afterward.
Creating a project (the wizard)
The new-project wizard lives at /projects/new (reached from the New project button on the dashboard or the New project sidebar item). It is a two-step card flow titled "New RAG project". On load it fetches /api/models (to know which providers you can use) and /api/projects (to detect duplicate names).
Step 1 - Name and documents
Project name
-
A single-line input with a hard limit of 20 characters (
maxLength={20}), accompanied by a live X/20 character counter that turns amber when you hit the cap. -
Duplicate-name detection is case-insensitive against your existing projects. If a match is found, the field is marked invalid and shows destructive helper text:
A project named '…' already exists - choose another name.
Description (optional)
- A two-row textarea. Optional and free-form.
Documents
- A drag-and-drop dropzone (also clickable to browse): "Drag and drop files here, or click to browse (max 50 MB each)."
- Any file with extractable text is accepted - rich formats are converted with MarkItDown, everything else ingests as plain text; opaque binary is rejected by the API. For reference, MarkItDown handles:
.pdf .docx .pptx .xlsx .xls .html .htm .csv .json .xml .txt .md .rtf .odt .ods .odp .epub .eml .jpg .jpeg .png .gif .bmp .tif .tiff .wav .mp3 .m4a .zip
- Per-file validation: a file over 50 MB toasts
{name}: exceeds the 50 MB limit. Selections are de-duplicated by name + size. - Selected files appear in a list, each with a per-file X remove button.
Buttons: Cancel returns to /dashboard; Configure advances to step 2 and is disabled until the name is non-empty and not a duplicate.

Step 2 - RAG configuration
This step is titled "RAG configuration".
Unavailable-key banner. If the embedding or answer provider you've selected has no usable key, an amber banner appears linking to Settings → API keys and mentioning that you can run a local Ollama model instead (no key required).
Chunking
| Field | Range | Notes |
|---|---|---|
| Chunk size | 100–8000 | Characters per chunk |
| Chunk overlap | ≥ 0 | Must be less than the chunk size |
Embedding model
- A select grouped by provider, displaying each option as
provider / model (Nd)whereNdis the embedding dimension. The list is filtered to providers you have a usable key for (your current selection is always kept even if it becomes unavailable). - Helper text: "Cannot be changed later without re-indexing." If your default provider lacks a key, the wizard auto-falls-back to the first available provider/model.
Answer model (LLM)
- A select grouped as
provider / model, availability-filtered, with the same auto-fallback behavior as the embedding picker.
Top-K
- A range slider from 1 to 20 with a live label "Top-K results: {n}". This is how many chunks are retrieved per query by default.
Upload progress. When files are attached and you submit, a progress row appears: "Uploading N files…" counting up, switching to "Processing on the server…" at 100%, with a percentage and a progress bar.
Buttons: Back returns to step 1. Create reads "Create & index N file(s)" when files are attached, otherwise "Create project"; it shows an inline loader while submitting and is disabled while submitting, when the name is taken, or when the embedding/LLM provider is unavailable.

What happens on create
The wizard issues a POST /api/projects, then (if you attached files) uploads them, then toasts and navigates you into the new project:
# 1. Create the project (owner JWT auth)
curl -X POST https://YOUR_HOST/api/projects \
-H "Authorization: Bearer YOUR_SUPABASE_JWT" \
-H "Content-Type: application/json" \
-d '{
"name": "Support Docs",
"description": "Customer-facing help center",
"chunk_size": 1000,
"chunk_overlap": 200,
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
"llm_provider": "openai",
"llm_model": "gpt-4o-mini",
"top_k": 5
}'The response is a ProjectOut (status 201). If files were attached, the wizard then uploads them to POST /api/projects/<project-id>/files (multipart) and shows a "Project created - indexing started" toast before pushing you to the project page.
{
"id": "<project-id>",
"name": "Support Docs",
"status": "empty",
"chunk_size": 1000,
"chunk_overlap": 200,
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
"embedding_dimensions": 1536,
"llm_provider": "openai",
"llm_model": "gpt-4o-mini",
"top_k": 5,
"file_count": 0,
"chunk_count": 0,
"query_count": 0
}Server-side validation mirrors the wizard guards and returns 422 for bad input: chunk_overlap must be < chunk_size; the embedding provider/model combination must be valid (it determines embedding_dimensions); and the LLM combination is validated. A duplicate name returns 409.
Default configuration values
If you don't change them, a project is created with these defaults:
| Setting | Default |
|---|---|
| Chunk size | 1000 |
| Chunk overlap | 200 |
| Embedding provider / model | openai / text-embedding-3-small (1536 dims) |
| Answer provider / model | openai / gpt-4o-mini |
| Top-K | 5 |
The project detail page
Opening a project navigates to /projects/<project-id>. The page reads two optional query parameters - ?file=<id> (forces the Files tab and highlights that file) and ?tab=<name> (opens a specific tab; valid tabs are files, memory, playground, api, visualize, settings).
Header. Shows the project name (with an indexing loader while status === "indexing") and a meta line:
{file_count} files · {chunk_count} chunks · {embedding_provider}/{embedding_model} · {llm_provider}/{llm_model}
The page auto-refreshes every 3 seconds while the project is indexing, and pushes fresh data back into the dashboard cache so cards stay current.
The six tabs at a glance
| Tab | What it does |
|---|---|
| Files | Upload, retry, re-index, and delete documents. Shows per-file status pills (Queued / Indexing / Indexed / Needs review), size, page count, and chunk count. Includes an Add files dialog and bulk Retry failed files / Re-index all files actions. |
| Memory | Read-only view of agent memories saved via the MCP server. Filter by content; each memory shows its tags, pin state, source, and date, with a per-row delete. |
| Playground | Test the exact RAG pipeline your API consumers use. Ask a question, see the grounded answer with model name and latency, and expand each cited source chunk. You can also switch the answer model inline (it instantly patches the project). |
| API | Create, revoke, and delete project API keys (oreag_sk_…), toggle per-key upload permission, and copy ready-made endpoint snippets (curl, JavaScript, the memory-graph URL, and the MCP connector command). |
| Visualize | Interactive 3D knowledge graph of the project's brain - files, chunks and memories as nodes with similarity edges. Rotate, zoom, hover for tooltips, click a node for details and a View file shortcut. |
| Settings | Edit project name and Top-K (instant), set the answer policy (how much evidence an answer needs, what language your documents are written in, what language answers are written in, a standing notice appended to every answer), turn on document version tracking, change the answer model and per-project key overrides, change chunking/embedding (triggers a re-index), and a danger zone to delete the project. |
For switching speed, the page mounts all tabs in the background 150 ms after load, so moving between them is instant.

Quick example: querying a project after creation
Once a project has indexed files and you've created an API key in the API tab, external callers query it through the public /v1 endpoint:
curl -X POST https://YOUR_HOST/v1/projects/<project-id>/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "How do I reset my password?"}'{
"answer": "To reset your password, open Settings and click ...",
"sources": [
{ "filename": "help-center.pdf", "page_number": 4, "chunk_index": 12, "content": "..." }
],
"model": "openai/gpt-4o-mini",
"latency_ms": 842
}The same pipeline (including the blending of agent memories into retrieval) runs in the Playground tab, just authenticated with your dashboard session instead of an API key.
Uploading Documents
Everything you query in Oreag starts with a document. When you upload a file, Oreag converts it to Markdown, splits it into chunks, embeds those chunks into your project's vector space, and makes them searchable through the RAG API, the playground, and the MCP tools. This page covers what you can upload, the size and quota limits, and the three ways to upload: the in-app Add files dialog, the public POST /v1/.../files REST endpoint, and the add_document MCP tool.
Supported file types
Oreag accepts any file that text can be extracted from. The rich formats below are converted with MarkItDown; every other extension (code, configs, logs, even files with no extension) is ingested as plain text. Only opaque binary is rejected - the same rule applies everywhere: the in-app dropzones, the public API, and the MCP tool.
| Category | Extensions |
|---|---|
| Documents | .pdf .docx .rtf .odt .epub .txt .md |
| Presentations | .pptx .odp |
| Spreadsheets | .xlsx .xls .ods .csv |
| Web / data | .html .htm .json .xml |
.eml | |
| Images | .jpg .jpeg .png .gif .bmp .tif .tiff |
| Audio | .mp3 .wav .m4a |
| Archives | .zip |
Conversion is performed by MarkItDown for the formats above and by plain-text decoding for everything else. Files that produce no extractable text fail during indexing with a conversion error; opaque binary files are rejected at upload with 400.
Images - AI captioning
Images (.jpg .jpeg .png) contain no machine-readable text - the words you see are pixels. During indexing Oreag asks your project's answer model to describe the image and transcribe any text in it verbatim; that caption becomes the file's searchable text. This requires the answer model to be OpenAI or Gemini (Gemini runs through its OpenAI-compatible endpoint) - projects on other providers fail image ingestion with a clear message asking to switch the answer model. Other image types (.gif .bmp .tif .tiff) yield only embedded metadata.
Audio - transcription with your own keys
Audio (.mp3 .wav .m4a) is transcribed with your own provider keys (project override or account key). Every speech-capable provider you hold a key for is tried in order, your project's own provider first:
| Provider | Transcription model |
|---|---|
| OpenAI | Whisper (whisper-1) |
| Gemini | native audio understanding |
| Groq | whisper-large-v3 |
| Mistral | Voxtral |
| Sarvam | Saarika (Indic languages) |
Providers without speech-to-text (Anthropic, xAI, DeepSeek...) are skipped. A free speech endpoint - suited to short, clear clips only - runs when none of your keys can transcribe. When that fallback is used the file carries a note, shown on its row in the Files tab and raised as a toast when indexing finishes, so you know your keys were not used and can add a speech-capable one.
Size and quota limits
| Limit | Value | Applies to | Enforced where |
|---|---|---|---|
| Max file size | 50 MB per file | Owner uploads and /v1 uploads | 413 on the API; toast in the UI |
| Max files per upload request | 20 | /v1 public uploads only | 413 |
| Max files per project | 1000 total | /v1 public uploads only | 413 |
| Upload rate | 60 files / minute per project (sliding 60s window) | /v1 public uploads only | 429 |
| Request rate (standard) | 120/min per key + 300/min per project | All /v1 endpoints except uploads | 429 + Retry-After |
| Request rate (heavy) | 10/min per key + 20/min per project | /v1 /explore and /memory-graph | 429 + Retry-After |
| Memories per project | ≤ 2000 | /v1 memory create | 413 |
Important: the per-request count, per-project total, and rate limits apply only to the public /v1 upload endpoint. Owner uploads made through the dashboard (the wizard and the Add files dialog) are not subject to those three quotas - only the 50 MB per-file ceiling applies to them.
Method 1 - The Add files dialog (dashboard)
The quickest way to add documents to an existing project is the Add files dialog on the project's Files tab.
Where to find it
- Open the project from the sidebar (
/projects/<project-id>). - Stay on the Files tab (the default).
- Click the Add files button in the header.


Selecting files
- Drag and drop files onto the dropzone, or click it to open the file browser. The dropzone reads "Drag & drop or click to choose (max 50 MB each)."
- Selection is validated immediately, per file:
- Over 50 MB → toast
"<name>: exceeds the 50 MB limit". - Duplicates (same name + size) are de-duped automatically.
- Over 50 MB → toast
- Selected files appear in a scrollable list; remove any one with the X next to it.
Chunking, embedding, and Top-K controls
The dialog defaults to the project's current settings, which you can adjust for this upload:
| Control | Range / behavior |
|---|---|
| Chunk size | 100–8000 characters (defaults to the project value) |
| Chunk overlap | ≥ 0 and must be less than chunk size (validated on submit) |
| Embedding model | Provider/model picker, filtered to providers you have a usable key for |
| Top-K | Slider 1–20 (project-wide setting) |
Embedding is project-wide. If you change the embedding model in this dialog, Oreag must re-embed everything so the whole project stays in a single vector space. An amber warning appears: "The embedding model is project-wide. Changing it re-indexes all N existing file(s) too." In that case the submit button reads "Add & re-index."
Submitting
While uploading, a progress row shows "Uploading N files…" and then "Processing on the server…" at 100%, with a percentage and progress bar. Closing the dialog (overlay click, Esc, or X) or clicking Cancel mid-upload aborts the in-flight request and toasts "Upload canceled."
On success you'll see one of two toasts:
- "…indexing started" (embedding unchanged), or
- "…re-indexing the whole project" (embedding model changed).
Method 2 - The public upload API (POST /v1/.../files)
External apps, scripts, and CI pipelines upload through the public REST endpoint using a project API key (oreag_sk_…).
Endpoint
| Method | POST |
| URL | https://<your-host>/v1/projects/<project-id>/files |
| Auth | Authorization: Bearer YOUR_API_KEY (must start with oreag_sk_) |
| Body | multipart/form-data, field name uploads (repeatable) |
The can_upload requirement (read-only vs. read+write keys)
Project API keys are read-only by default. A key can upload files only if its can_upload ("Uploads") flag is enabled. If you call this endpoint with a read-only key, the request is rejected with 403 - "read-only."
To enable uploads, open the project's API tab, find the key's row, and check the "Uploads" checkbox in the Access column (the change saves optimistically). You can also create or manage keys there.

Validation and limits (in order)
The endpoint applies these guards on every request:
- Key must have
can_upload = true→ else403. - At least one file must be attached → else
422. - No more than 20 files in this request → else
413. - Project total after this upload must stay ≤ 1000 files → else
413. - Rate limit: files created in the last 60 seconds plus this batch must be ≤ 60 → else
429. - Text must be extractable from each file → else
400. - Each file must be ≤ 50 MB → else
413.
Public uploads always use the project's default chunking and embedding settings - there are no per-request chunking or embedding overrides on /v1. (Use the dashboard Add files dialog or the /reindex endpoint to change those.)
On success the endpoint returns 201 with the list of newly created files, the project status moves to indexing, and background ingestion is queued.
curl example
curl -X POST "https://<your-host>/v1/projects/<project-id>/files" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "uploads=@./handbook.pdf" \
-F "uploads=@./faq.md" \
-F "uploads=@./pricing.xlsx"The key here must have "Allow uploads" enabled - read-only keys cannot ingest and will get a
403.
Example 201 response
[
{
"id": "c1a2b3c4-0000-4444-8888-aaaaaaaaaaaa",
"filename": "handbook.pdf",
"status": "pending",
"content_type": "application/pdf",
"source_extension": ".pdf",
"size_bytes": 482113,
"page_count": 12,
"chunk_count": 0,
"error": null,
"conversion_error": null,
"created_at": "2026-06-20T10:15:00Z",
"indexed_at": null
}
]Files start as pending and progress through processing to indexed (or failed). If the project has Track document versions enabled, a file that looks like a new version of one already in the project comes back as review instead: it is stored but not indexed until someone confirms what it replaces in the dashboard. Poll for a terminal status, not for indexed alone. Poll GET /v1/projects/<project-id> for the project's status, or list files via the dashboard, to track progress. See Files & indexing for the full status lifecycle, and When to turn version tracking on for the kinds of document the review step exists for.
JavaScript example
const form = new FormData();
form.append("uploads", fileA, "handbook.pdf");
form.append("uploads", fileB, "faq.md");
const res = await fetch(
"https://<your-host>/v1/projects/<project-id>/files",
{
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY" }, // do NOT set Content-Type; the browser sets the multipart boundary
body: form,
}
);
if (res.status === 403) throw new Error("This API key is read-only - enable Uploads on the key.");
if (res.status === 429) throw new Error("Upload rate limit hit (60/min per project) - back off and retry.");
if (!res.ok) throw new Error(`Upload failed: ${res.status}`);
const files = await res.json();
console.log(`Queued ${files.length} file(s) for indexing.`);Status codes summary
| Code | Meaning |
|---|---|
201 | Files accepted and queued for indexing |
400 | No text could be extracted from a file (opaque binary) |
403 | API key is read-only (can_upload is false) |
413 | A file exceeds 50 MB, >20 files in request, or >1000 files in project |
422 | No files attached |
429 | Upload rate limit exceeded (60 files/min per project) |
401 | Missing or invalid API key |
Method 3 - The MCP add_document tool (coding agents)
When a coding agent (Claude Code, Codex, claude.ai/Desktop) is connected to your project through the Oreag MCP server, it can add documents directly with the add_document tool. This is ideal for letting an agent persist generated notes, specs, or summaries into the project's searchable knowledge base.
| Param | Type | Description |
|---|---|---|
filename | str | Name of the document. Any extension (or none) works. |
content | str | The full text body of the document. |
Under the hood the tool sends a multipart upload to the same public endpoint:
POST /v1/projects/<project-id>/files
files = { "uploads": (filename, content (utf-8), "text/plain") }The MCP key needs upload permission. Because add_document calls the public upload endpoint, the project API key the MCP server uses must have can_upload enabled. With a read-only key the tool fails with 403, just like a raw API call. The same 50 MB / 20-per-request / 1000-per-project / 60-per-minute limits apply.
Example agent call (conceptual)
{
"tool": "add_document",
"arguments": {
"filename": "architecture-decision-2026-06.md",
"content": "# ADR-014: Adopt pgvector\n\nWe chose pgvector because..."
}
}The document is chunked, embedded, and becomes searchable through search_docs, ask_docs, and explore_brain once indexing completes. To connect the MCP server, see the MCP connector snippet on your project's API tab.
What happens after upload
Regardless of which method you use, every uploaded file goes through the same pipeline:
- Convert the source to Markdown (MarkItDown).
- Split into chunks using the project's (or per-file) chunk size and overlap.
- Embed each chunk in batches into the project's vector space.
- Index - the file moves to
indexed, itschunk_countandindexed_atare set, and the project status is recomputed.
If any step fails, the file is marked failed with an error (or conversion_error) message. See Files & indexing for how to monitor, retry, re-index, and delete files.

Files & Indexing Management
Once documents are uploaded, the project's Files tab is where you watch them index, retry failures, re-process the whole project, and delete what you no longer need. This page explains the file status lifecycle, how indexing works, and the retry / re-index / delete actions - from both the dashboard and the API where applicable.
The file status lifecycle
Every file moves through five statuses:
| Status | Meaning | UI pill (Files tab) |
|---|---|---|
pending | Uploaded and queued, not yet picked up | Queued (clock icon, amber) |
processing | Currently converting / chunking / embedding | Indexing (animated loader, sky) |
indexed | Successfully chunked, embedded, and searchable | Indexed (check, muted) |
failed | Conversion or indexing error; see the error text | Needs review (alert, red) |
review | Looks like a new version of a document already in the project. Converted and stored, but not chunked or searchable until you confirm what it replaces. Only appears when Track document versions is on for the project - see When to turn version tracking on below. | Confirm version (branch, violet) |
Each pill carries an aria-label/title for accessibility.
Project status (derived from its files)
The project's own status is recomputed from its files:
| Project status | Condition |
|---|---|
empty | No files - or files exist but none are searchable (every one is awaiting a version decision or superseded) |
indexing | Any file is pending or processing |
error | Any file is failed (and none pending/processing) |
ready | At least one file is indexed and in force |
In the UI the project's status is shown as a colored dot/capsule: gray (empty), amber (indexing), emerald/green (ready), red (error).
Server restarts: if the backend restarts while files are mid-flight, any
pendingorprocessingfiles are automatically markedfailedwith the error "Interrupted by server restart." Just retry them (below).
How indexing works
When a file is queued, a background task runs the pipeline:
- Convert the source document to Markdown.
- Split it into chunks using the file's chunk size/overlap (falling back to the project's values).
- Embed the chunks in batches and bulk-insert them as vectors.
- Finalize - status becomes
indexed,chunk_countandindexed_atare set, and the project status is recomputed.
If a document produces no chunks (e.g., no extractable text), or any step raises an error, the file is rolled back to failed and the message is stored in error (indexing errors) or conversion_error (Markdown-conversion errors) - each shown distinctly on the file row.
Live progress in the dashboard
The Files tab and project page auto-refresh every 3 seconds while any file is pending/processing (or while the project status is indexing), so you can watch files flip to Indexed without reloading. The project header shows an animated loader while indexing.
File row details
Each file row shows its icon, filename (truncated), and a meta line:
EXT · size · N page(s) · M chunk(s)
For example PDF · 1.2 MB · 12 pages · 34 chunks. Page counts appear for PDFs only. Destructive error and conversion_error text appear beneath the meta line when present.

Retrying failed files
When a file lands in Needs review (failed), retry it to re-run the pipeline.
Retry a single file (dashboard)
- On the file's row, open the per-file 3-dots menu.
- Click "Retry indexing" (shown when the file is
failed). - The file resets to
pending, its errors clear, and indexing re-queues.
The menu item is disabled while a file is processing.
Retry all failed files at once
- In the Files tab header, open the 3-dots menu (header level).
- Click "Retry failed files."
- Oreag filters to
failedfiles and retries each in parallel. If there are none, you'll see "No failed files to retry"; otherwise "Retrying N failed file(s)."
Retry via API
| Method / URL | POST /api/projects/<project-id>/files/<file-id>/retry |
| Auth | Owner JWT (dashboard) |
| Behavior | 404 if the file isn't in the project; 409 if the file is currently processing; otherwise resets to pending, clears errors, re-queues. |
| Response | The updated FileOut |
curl -X POST "https://<your-host>/api/projects/<project-id>/files/<file-id>/retry" \
-H "Authorization: Bearer YOUR_SUPABASE_JWT"Re-indexing the whole project
Use re-index when you change something project-wide - a different embedding model, or new chunk size/overlap - that requires every file to be re-processed so the entire project stays in one consistent vector space.
Re-indexing wipes all existing chunks, resets every file to
pending, sets the project toindexing(oremptyif there are no files), and re-queues all files. Queries are unavailable until indexing completes.
Matryoshka fast path (no re-embedding): shrinking the same MRL-capable model to a smaller
embedding_dimensions(e.g.text-embedding-3-large3072 to 1024) skips all of the above - the stored vectors (chunks and memories) are truncated to the prefix and re-normalized in place, instantly and at zero API cost. Growing back, or switching models, still needs the full re-index. On a model switch, memory embeddings are cleared immediately (old-model vectors are incompatible) and re-embedded with the new model in the background.
Re-index from the Files tab
- Open the Files tab header 3-dots menu.
- Click "Re-index all files."
- Oreag posts to the re-index endpoint with an empty body (keeping current settings) and toasts "Re-indexing started."
Re-index from Settings (to change model / chunking)
The Settings tab is where you change the embedding model or chunk size/overlap. In the Indexing & embedding card:
- If you changed the embedding model or chunk settings, the action button reads "Change & re-index" and opens a "Re-index all files?" confirmation: "All N file(s) will be re-chunked and re-embedded…"
- Confirm with Re-index to apply the new settings and re-process every file. Toast: "Re-indexing started - all files will be processed again."
Changing only the provider key (not the model or chunking) is an instant save with no re-index.


Re-index via API
| Method / URL | POST /api/projects/<project-id>/reindex |
| Auth | Owner JWT |
| Body | ReindexRequest - optional embedding_provider, embedding_model, embedding_dimensions, embedding_api_key, chunk_size, chunk_overlap (overlap must stay < chunk_size, else 422) |
| Behavior | Wipes all chunks, resets every file to pending, status → indexing, re-queues all |
| Response | list[FileOut] |
# Re-index keeping current settings (empty body)
curl -X POST "https://<your-host>/api/projects/<project-id>/reindex" \
-H "Authorization: Bearer YOUR_SUPABASE_JWT" \
-H "Content-Type: application/json" \
-d '{}'# Re-index AND change chunking + embedding model
curl -X POST "https://<your-host>/api/projects/<project-id>/reindex" \
-H "Authorization: Bearer YOUR_SUPABASE_JWT" \
-H "Content-Type: application/json" \
-d '{
"embedding_provider": "openai",
"embedding_model": "text-embedding-3-small",
"chunk_size": 1200,
"chunk_overlap": 150
}'Note on uploads that trigger re-indexing: uploading through the dashboard Add files dialog (or the owner upload endpoint) with a changed embedding provider/model also wipes all chunks and re-queues every existing file - embedding is a project-wide setting. The public
/v1upload endpoint never changes embedding settings, so it never triggers a project-wide re-index.
When to turn version tracking on
Track document versions never looks at the file format. A PDF, a DOCX and a plain-text file are all asked the same question - is this upload a new edition of something already in this project? - and no gate anywhere tests the extension. So document type here means what the upload is to the document it matched: a later edition of it, a notice about it, a part of it. The dialog asks it as What kind of document is this?
Six things decide whether an upload is held for that question:
- Track document versions is on for the project (Settings → Answer policy → Track document versions). It is off by default.
- Document version extraction is enabled for the deployment. This is a fleet-wide kill switch: when an administrator turns it off, the project switch is disabled and no upload is held anywhere on the instance.
- The file is not already part of a document's history.
- The file has never indexed successfully. Retrying or re-indexing a file that already indexed never sends it back for review.
- The project already holds something to match against - at least one other file that is neither itself awaiting review nor already superseded. The first upload into an empty project is never held, and files uploaded in one batch cannot match each other while they are still parked, so importing a corpus all at once produces fewer matches than adding the same files one by one.
- The extraction call succeeded. It runs on the project's own provider key and fails open: no usable key, a provider outage or an unreadable reply all index the upload as its own document rather than holding it.

What the Confirm version dialog asks
Three things, in this order:
- Is this a version at all? - A new version of
<filename>, or A separate document. The second indexes the upload on its own, affects nothing else, and is how a file parked by mistake is released. - What does it do to that version? - the relation. This is the answer that decides whether the earlier document keeps answering questions.
- What kind of document is this? - the kind. This describes the upload itself.
One test settles most of the hard cases: does the upload contain the thing it relates to, or only talk about it? A document that sets out the whole agreement, statute or standard in its new form is a later edition of it, however its title reads - an "Amendment Act" that reproduces the entire amended statute restates it. Only a document that carries changes by reference, and would be unreadable without the document it changes, is Amends another document.

Which documents it fits
| The document you upload | Kind | Relation | What happens |
|---|---|---|---|
| A consolidated reprint, a re-enacting statute, a new edition of a standard, the journal version of a preprint, a final report replacing an interim one | Consolidated / later edition of one | Replaces it | The earlier one stops answering questions. |
| An amended-and-restated agreement, or a full revision | Consolidated / later edition of one | Restates it in full | The earlier one stops answering questions. Recorded distinctly from Replaces it because a restatement carries obligations an ordinary revision does not. |
| The following year in an annual series, a standard edition published yearly, an API version whose predecessor is still supported, a contract amendment that sets out its own new terms alongside an MSA that stays in force | A document in its own right | Comes after it, both still valid | Both keep answering. This is the right choice whenever the earlier document is plausibly still in use. |
| An amending Act, or a contract amendment, that carries only instructions - "in section 135, for the words X substitute Y" - and never reproduces what it changes | Amends another document | Amends it | The earlier one keeps answering and is marked amended. The amendment is stored but not searchable, so its diff text cannot be quoted as if it were the rule. |
| An erratum, a corrigendum, a Department of Error notice | Corrects another (erratum, retraction) | Corrects it | The earlier one keeps answering. The notice is stored but not searchable. |
| A retraction notice, where the withdrawn document should stop answering | A document in its own right | Retracts it | The earlier one stops answering and is marked retracted. It stays downloadable rather than disappearing. The notice itself is stored but not searchable. |
| The same edition of a document in another language | Translation of another | Translates it | Both keep answering. The original is not retired. |
| An appendix, supplementary material, an annex published separately | Part of another (appendix, annex) | Is part of it | Both keep answering. |
| Something whose relationship you can see but whose kind you cannot | Not sure | Any of the above | Whatever the relation you chose does. Not sure is the one kind with no safety rail - see below. |
Four of those kinds describe a document that refers to another rather than replacing it: Amends another document, Corrects another (erratum, retraction), Translation of another and Part of another (appendix, annex). None of them can retire the document they point at. The dialog warns in place, and POST /api/projects/<project-id>/files/<file-id>/version enforces the same rule independently of the form, answering 422 when one of those four kinds is paired with Replaces it, Restates it in full or Retracts it - whether the request comes from the dashboard or from your own code. That is why a retraction notice recorded as Corrects another (erratum, retraction) leaves the retracted work answering: only a kind outside those four - A document in its own right, Consolidated / later edition of one or Not sure - can withdraw another.
Not sure is deliberately outside that list, so it leaves Replaces it, Restates it in full and Retracts it available and unwarned. Use it when you are sure of the relation and only the kind is unclear. If the upload is a notice about another document - an erratum, a translation, an annex - name that kind instead, so the rail is there when you need it.
Two more things matter if you call the endpoint yourself. relation_kind is not optional in effect: omit it on a request that names a predecessor and it is read as supersedes, which retires that predecessor, so send the relation explicitly on every call. And Replaces it, Restates it in full and Retracts it are the only relations that require in_force_from - it becomes the retired edition's end date and is never invented, so omitting it is a 422, and a date earlier than the edition being replaced is a 422 too. The four relations that retire nothing need no date.
Where it is a poor fit
The test is not what field the corpus comes from, it is whether an upload ever makes an earlier upload wrong. Turn it on where a later document is meant to be read instead of an earlier one that is still in the project: statutes and their reprints, standards, journal articles and their errata, contracts and their restatements, internal policies reissued with an effective date, annual series. Leave it off where every file stands on its own and _v2 means someone saved a draft: meeting notes, reports, decks, working documents. A corpus can be both; if only part of it has editions, keep that part in its own project.
The question asked on upload - is this a new edition of something already here? - is just as true of report_v2.pdf as of an amending Act. That is why the switch is per project and off by default: a fleet-wide setting would hold uploads in projects that have nothing to do with editions. On a misfit corpus, uploads are parked for a decision that changes nothing, none of them are searchable until someone makes it, and each one has already spent an extractor call on the project's own provider key - the opening 6,000 characters of every file, read by a model to reach that decision.
The cost is visible on the projects list: a project whose files are all waiting for a version decision reports its status as empty, not ready. Nothing in it is searchable, so ready would tell the dashboard and /v1 that the project can answer when it cannot. A project with some indexed files and one file in Confirm version still reports ready.
Switching it back off is not an undo. It stops new uploads being held, but every file already in Confirm version stays there. Re-indexing a held file is refused with a
409("This file is waiting for a version decision"), and a project re-index skips it. Each one has to be cleared by hand - choose A separate document to index it on its own - or deleted. There is no bulk action. Try the switch on a small project before turning it on over a corpus you have already uploaded.
The reviewer chooses, and nothing is deleted. The match itself is never pre-answered: the dialog opens with is this a version at all? undecided, because on a measured corpus the proposal was wrong for 28 of the 37 documents that should not have matched at all - errata, translations, supplements. Answer yes and the two dropdowns below arrive pre-filled with what was proposed, or, where nothing was proposed, with Not sure and Replaces it - the pairing that retires the earlier document. Read both before confirming. Nothing is deleted either way: Deleting a file below sets out what a superseded version keeps, and how that differs from deleting it.
Deleting a file
Deleting a file removes the file row, cascades to delete its chunks, and removes both the source and the converted-Markdown objects from storage. The project status is then recomputed.
Where Version history appears
The per-file 3-dots menu shows Version history only on a file that has one - that is, a file sharing a document with at least one other file in the project, or a file still waiting for a version decision. A file uploaded once and never replaced has a history of exactly itself, so the entry is not offered: opening it would list that single file and imply editions exist where none do.
This is independent of the Track document versions switch in both directions. Turning it on does not put the entry on files that have no editions, and turning it off does not hide the history of files that do.
Superseding is not deleting. When a new version replaces an older one, the older one keeps its row, its original file and its converted text - it only loses its chunks, so it stops being searchable while staying downloadable and re-indexable. Deleting is still permanent, and deleting the current version of a document does not automatically promote an earlier one: the remaining versions stay in the project, unsearchable, until you make one current from Version history.

Delete from the dashboard
- On the file's row, open the per-file 3-dots menu.
- Click "Delete file" (destructive).
- Confirm in the "Delete this file?" dialog (it names the file). While deleting, a loader animation plays with "Permanently deleting…" (no close button); the dialog closes and toasts "<filename> deleted" once the deletion completes.
Delete via API
| Method / URL | DELETE /api/projects/<project-id>/files/<file-id> |
| Auth | Owner JWT |
| Behavior | 404 if the file isn't in the project; deletes the file (cascading chunks), removes source + Markdown from storage, recomputes status |
| Response | 204 No Content |
curl -X DELETE "https://<your-host>/api/projects/<project-id>/files/<file-id>" \
-H "Authorization: Bearer YOUR_SUPABASE_JWT"File deletion is an owner-only dashboard/JWT action. The public
/v1API can upload files but does not expose a file-delete endpoint - project API keys cannot remove files.
Quick reference
| Action | Dashboard path | API endpoint | Auth |
|---|---|---|---|
| List files | Files tab | GET /api/projects/<project-id>/files | Owner JWT |
| Retry one file | File row ⋯ → Retry indexing | POST /api/projects/<project-id>/files/<file-id>/retry | Owner JWT |
| Retry all failed | Header ⋯ → Retry failed files | (per-file retry, in parallel) | Owner JWT |
| Re-index project | Header ⋯ → Re-index all files / Settings → Change & re-index | POST /api/projects/<project-id>/reindex | Owner JWT |
| Delete a file | File row ⋯ → Delete file | DELETE /api/projects/<project-id>/files/<file-id> | Owner JWT |
Querying your project
The query endpoint is Oreag's chat-style RAG entry point. You send a natural-language question; Oreag embeds it, retrieves the most relevant document chunks from the project, blends in any relevant agent memories, and asks the project's configured LLM to write a grounded answer that cites its sources. This is the endpoint your application's "Ask" feature should call.
If you only want raw passages and intend to run your own LLM (or no LLM at all), use
/retrieveinstead. If you want to follow how knowledge connects across documents and memories, use/explore.
Evaluation playground
Open a project → Playground → Evaluator. The evaluator has its own workspace with Back to conversation, rather than a second tab bar. Conversation and unsaved evaluation edits remain available while you stay in the project.
Configure a baseline and an optional challenger independently:
- Answer model, using the available provider models.
- Embedding model and its supported vector dimensions. Changing the embedder resets the dimension to its native default; the backend validates every model/dimension pair and every returned vector.
- Top-K (1–20), minimum similarity (0–1), required strong sources (0–20), and optional translation similarity floor (0–1).
- Hybrid keyword/semantic search, indexed memories, document language, answer language and its strict/default behavior, and an optional answer notice.
The evaluator snapshots the project's already indexed passages and prepares separate vectors for the configurations. It does not change live project settings, re-chunk original files, or replace production vectors. Identical embedder/model/dimension configurations reuse the first variant's prepared vectors within a run. Other configurations re-embed the same text. Each run supports up to 2,000 passages / 8 MB of snapshot text and up to 20 questions, with one or two configurations. The latest 20 unarchived runs are retained. Creating another run automatically archives the oldest eligible finished run, releasing its frozen corpus and prepared vectors while preserving compressed answers, configurations, feedback and metrics. Active runs and reference runs are protected. If every slot is active or protected, creation returns 409.

Save set stores the questions and configurations in evaluation_suites, protected by project ownership. Saving uses a revision check to prevent overwriting another session's edits. Load saved set discards local edits and reloads the database copy. Run comparison saves the set, creates an immutable run snapshot in evaluation_runs, and advances it through small work steps. Answers, source passages, model settings, scores and progress survive reloads and are available under Saved runs. Export test set and Export results download JSON; importing a version-1 local test set upgrades its Top-K variants using the current project models, and version-2 imports preserve full configurations. Imports allow up to 256 KB and replace the current unsaved draft.
Each question can require expected text (contains or exact match) and/or an expected source filename. Text checks normalize Unicode compatibility characters, case and whitespace; source checks compare returned filenames. Both supplied rules must pass. Questions without expectations are marked Review. These are explicit rule checks, not semantic correctness scores; inspect answers for factual accuracy. Helpful/not-helpful ratings and optional notes are saved with each evaluation result once the run stops. They do not enter live answer feedback or Health metrics.
Evaluation bypasses L1/L2 answer caches and uses separate language-detection and translation caches. It shares agentic planning, generation and grounding, using isolated cosine and optional full-text retrieval over the frozen snapshot. Memory passages participate in the same snapshot ranking when included. Original chunk boundaries are held constant.
Evaluation is separate from your live project. Editing or saving configurations, running comparisons, scoring answers and submitting evaluation feedback never updates Project Settings, production files or vectors, live query history, or Health and live query/cache metrics. Evaluation answers have query_id: null; scores and feedback remain in the saved run. There is no automatic apply-to-project action: change production configuration explicitly in Project Settings when you decide to adopt a result.
Normal provider charges and quota consumption still apply to embedding and generation. Metered evaluation_index / evaluation_query events appear in Usage and contribute to account totals. Evaluations also share the documented API rate limits.

New runs are driven by a durable backend worker and continue when the browser is closed. Reopen Saved runs to inspect progress. Existing manual runs can still be advanced from the workspace or API. Stop run cancels the remaining queue. Cancelled runs are final; start a new run to repeat them. A lease prevents simultaneous advance calls; after a worker crash its lease expires after 10 minutes. Failed steps may be resumed. A provider call whose result could not be committed can be billed again when retried.
Evaluation API
Use a project API key with the existing Authorization: Bearer header. All operations are scoped to its project and enforce revocation and suspension checks. Standard per-key/project rate limits apply; creating a public run and manual advance calls also use the heavy-operation budget. Background steps check project/account suspension and the originating API key before performing work. Dashboard endpoints use the same suffixes under /api/projects/{project_id}/evaluations with owner authentication.
| Method | Public path | Purpose |
|---|---|---|
| GET, PUT | /v1/projects/{project_id}/evaluations/suite | Read {revision, suite}; save with that revision and a version-2 suite. Stale revisions return 409. |
| GET, POST | /v1/projects/{project_id}/evaluations/runs | List run IDs/status/timestamps; create with {id, suite, background: true, reference_run_id: null} (background defaults to true). Generate a UUID id once and reuse it when retrying an uncertain create. |
| GET, DELETE | /v1/projects/{project_id}/evaluations/runs/{run_id} | Read a complete run; delete a finished/stopped run and its vectors after any active step finishes. |
| POST | /v1/projects/{project_id}/evaluations/runs/{run_id}/advance | Prepare up to 32 vectors or answer one question; returns the updated run. Concurrent steps return 409. |
| POST | /v1/projects/{project_id}/evaluations/runs/{run_id}/cancel | Cancel remaining work. |
| POST | /v1/projects/{project_id}/evaluations/runs/{run_id}/resume | Reset a failed run; background runs resume automatically. Manual runs still require advance. |
| PUT, DELETE | /v1/projects/{project_id}/evaluations/runs/{run_id}/results/{result_index}/feedback | Rate a saved result or clear its rating and note. Uses the zero-based index in the run’s results array. |
Evaluation feedback uses {"rating":"helpful","note":"Matches the expected policy"} (or not_helpful; optional note up to 1,000 characters). PUT replaces the rating and note and returns feedback_rating and feedback_note; DELETE returns 204. The fields also appear on that result when reading/exporting its run. Feedback requires a completed, failed or cancelled run; an active run returns 409. Use this evaluation endpoint rather than the normal query feedback endpoint.
The suite JSON uses version: 2, cases (up to 20 objects with id, question, expected, match, source) and variants (one or two configuration objects). Each variant requires llm_provider, llm_model, embedding_provider, embedding_model, and embedding_dimensions; optional fields are top_k, min_similarity, min_strong, cross_lingual_floor, hybrid_search, include_memories, document_language, answer_language, answer_language_strict, and answer_disclaimer. Export a test set from the evaluator for a complete validated example. Provider credentials are resolved from the owner account or a matching project override and are never stored in test sets or run snapshots.
Run states are preparing, running, completed, failed, and cancelled. Poll GET on the run while preparing/running, or subscribe to evaluation webhooks. Set background: false only for a caller-driven run, then call advance sequentially. Respect 429 backoff instead of retrying in a tight loop. These endpoints do not change the existing public query request or answer response.
Endpoint
| Method & path | POST /v1/projects/<project-id>/query |
| Auth | Authorization: Bearer YOUR_API_KEY (a project API key, format oreag_sk_…) |
| Content-Type | application/json |
| Logged? | Yes - each call writes a QueryLog (question, top_k, latency) for the project's query count. |
The API key scopes the call to exactly one project, so the project id in the URL must be the project that owns the key. A read-only key works fine here - querying never requires upload permission.
Request body
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
question | string | yes | 1–4000 chars | The natural-language question to answer. |
top_k | integer | no | 1–20 | How many document chunks to retrieve as context. Defaults to the project's configured Top-K (set in the project's Settings). The effective value is always capped at 20. |
conversation_id | string | no | ≤ 128 chars | Optional thread id. Pass it to continue a conversation so follow-ups like "summarize that" are resolved against earlier turns. Omit it for a stateless one-off query. |
{
"question": "What is our refund window for enterprise customers?",
"top_k": 8
}Response body (QueryResponse)
| Field | Type | Description |
|---|---|---|
answer | string | The generated answer. The system prompt instructs the model to answer strictly from the retrieved context, to say it doesn't know when the context lacks the answer, and to cite sources inline as [1], [2], … |
sources | array of SourceChunk | The chunks (and blended memories) that grounded the answer, sorted by similarity descending. |
model | string | The provider/model that produced the answer, e.g. "openai/gpt-4o-mini". |
latency_ms | integer | End-to-end server latency in milliseconds. |
depth | string | How the question was classified - "short" (concise, strictly-grounded answer) or "long" (comprehensive, structured answer for exam-style / multi-part questions). |
sub_queries | array of string | The focused sub-queries a long question was decomposed into and retrieved for. Empty for short questions. |
needs_clarification | boolean | true when grounding was too thin to answer and Oreag is asking for clarification instead. When true, answer holds the human-facing clarification prompt. |
clarification_questions | array of string | The follow-up questions to put to the user when needs_clarification is true; empty otherwise. |
conversation_id | string | null | Echoes the thread id when you passed conversation_id; null for a stateless query. |
cache_layer | string | null | Which cache layer served the answer - "l1" for an exact-match (L1) hit, "l2" for a semantic (L2) hit, null when the answer was computed fresh. |
cache_similarity | float | null | On an "l2" hit, the cosine similarity between your question and the cached question it matched; null otherwise. |
retrieval_similarity | float | null | Mean similarity of the sources behind this answer - a confidence indicator for the retrieval that grounded it. null, never 0, when nothing was retrieved (a clarification): not measured is a different fact from matched at 0.0. |
Each SourceChunk:
| Field | Type | Description |
|---|---|---|
filename | string | Source file name. Blended memories appear as "memory". |
page_number | integer | null | Page number for paginated sources (PDFs); null otherwise. |
chunk_index | integer | Position of the chunk within its file. Blended memories use -1. |
content | string | The chunk text that was shown to the model. |
similarity | float | Cosine similarity (0–1) between the question and this chunk. |
cited | boolean | true when the answer actually referenced this block inline as [n]. sources is everything retrieval returned, which is deliberately wider than what the answer used - without this flag a reader cannot tell supported the claim from was in the context and ignored. |
{
"answer": "Enterprise customers have a 30-day refund window from the invoice date [1]. Refunds are prorated for annual plans [2].",
"sources": [
{
"filename": "enterprise-policy.pdf",
"page_number": 4,
"chunk_index": 12,
"content": "Enterprise customers may request a refund within 30 days of the invoice date...",
"similarity": 0.83
},
{
"filename": "memory",
"page_number": null,
"chunk_index": -1,
"content": "Decision: annual enterprise refunds are prorated, agreed in the Q2 pricing review.",
"similarity": 0.41
}
],
"model": "openai/gpt-4o-mini",
"latency_ms": 1342,
"depth": "short",
"sub_queries": [],
"needs_clarification": false,
"clarification_questions": [],
"conversation_id": null,
"cache_layer": null,
"cache_similarity": null
}How memory is blended into answers
A project's document chunks and its agent memories (saved by MCP-connected agents) live in the same per-project embedding space, so Oreag can compare them with one cosine operator. On every query:
- Oreag retrieves the top-
kdocument chunks for the question. - It then searches up to 4 of the project's embedded memories (
rag_memory_blend_k = 4). - Any memory whose similarity to the question is ≥ 0.35 (
rag_memory_min_similarity) is appended to the context as a pseudo-source -filename = "memory",chunk_index = -1. - All sources (chunks + qualifying memories) are sorted by similarity descending and handed to the LLM together.
This blending is best-effort and silent: if the project has no embedding key, or no memory clears the threshold, the memory step is simply skipped and you get a documents-only answer. Memories stored without an embedding (e.g. saved while no embedding key was configured) are not searchable and never blended.
The agentic query loop
Query is more than a single retrieve-then-answer pass - it adapts to how big the question is:
- Automatic depth detection. Oreag classifies each question as short or long with a pure heuristic (no model call): a marks weighting (e.g. "13 marks") or a directive verb like explain, discuss, describe, compare, analyze, evaluate, outline makes it long; anything else is short. The chosen depth comes back in the
depthfield. - Sub-query decomposition. For a long question, Oreag asks the LLM to break it into a handful of focused sub-queries (the original question is always kept too). The sub-queries are returned in
sub_queries. - Multi-round retrieval. It retrieves every sub-query, merges and de-duplicates the results (keeping the best similarity per source), and checks whether the grounding is sufficient. If it is too thin, Oreag broadens and retries rather than giving up - up to a small number of rounds.
- Depth-aware answer. A long question gets a comprehensive, structured answer that still uses whatever partial context was found (it won't refuse just because coverage is incomplete); a short question keeps the concise, strictly-grounded answer.
- Human-in-the-loop clarification. If grounding is still too thin after the retries, Oreag does not invent an answer or return an empty one. Instead it sets
needs_clarificationtotrue, puts a human-facing clarification prompt inanswer, and lists the specific follow-up questions inclarification_questions.
Asking in a different language from your documents
Oreag answers in the language the question was asked in, even when every document is in another language. Ask an English handbook a question in Tamil and the answer comes back in Tamil. Nothing needs configuring for this - it is the default.
Two separate mechanisms make that work, and it helps to know they are separate, because they have different limits.
1. The answer's language
By default the model is instructed to "write the answer in the same language the question was asked in, even when the source material is in another language." A project can change that in Settings → Answer policy → Answer language, which has three settings rather than two:
| Setting | What every answer is written in |
|---|---|
| Match the question (default) | the language each question was asked in |
| A language, Always use it on | that language, whatever the question used |
| A language, Always use it off | the question's language, falling back to that one |
Pick Always when the audience is fixed - a public help centre, a regulator-facing assistant. Turn it off to make the language a house default that a reader writing in Hindi or Tamil still overrides. Leave it on Match the question when you have no house language at all.
Two details worth knowing, both measured rather than assumed:
- Always really is always. The instruction is repeated at the end of the prompt as well as the start, because in the system prompt alone a question asked in Hindi came back in Hindi despite it - 3 out of 9 answers honoured a pinned language, and 9 out of 9 do now.
- Matching is reliable even when the sources disagree. When the passages Oreag retrieves are written in a script the question is not, it identifies the question's language and names it, because "answer in the language of the question" alone was followed only 12 times in 18. Naming it scores 18/18. That costs one small model call, only in that situation, cached per question.

2. Finding the passage in the first place
Before it can answer in Tamil, retrieval has to find the English passage
from a Tamil question. That is the embedding model's job, and it is where
cross-lingual search actually gets hard. Measured on
openai/text-embedding-3-large, asking a question in a script the documents
do not use ranked the correct passage like this:
| Question language | Where the right passage ranked |
|---|---|
| French, Spanish, Japanese, Russian, Arabic, Chinese, Korean, Hindi and 24 others | first |
| Gujarati, Marathi, Thai | second |
| Sinhala, Malayalam, Lao | third to fifth |
| Khmer, Burmese | last of six |
A Burmese question scored 0.00 similarity against the English passage that answered it, where an English control scored 0.75 - the embedder barely represents Burmese at all. The keyword half of hybrid search cannot rescue it either: an English full-text index contains no Khmer words, so it matched nothing on any non-English question.
So Oreag searches with a translation of the question. The question itself is never altered, which is why the answer still comes back in the user's own language. Measured across all 40 languages the menu offers, this took first-place retrieval from 32/40 to 40/40.
The rewrite runs only when two conditions hold, and both matter:
-
The question and your documents are in different languages. A Hindi corpus asked in Hindi is left alone - that path already works, and translating it would break it. This is checked in both directions: a Hindi question against English documents, and equally an English question against Hindi, Tamil, Arabic, Thai or any other non-Latin corpus. The reverse direction is decided from the Document language you set in Settings, so a project that has not set one will not use it.
This also covers questions typed in English letters but another language - "refund policy kya hai", "refund policy enna". Those are Latin script, so nothing about their spelling says they are not English, and Oreag recognises them from a short list of words that carry no English meaning. Getting it wrong in the cautious direction is deliberate: a question it does not recognise is simply searched as typed, exactly as before. A plain English question against English documents costs nothing at all: no extra model call is made.
-
Searching with the question as typed came back weak. When an embedding model has no useful grasp of a language, every chunk scores near zero, because the query vector points nowhere. That is measurable, so Oreag looks before it pays.
Romanized questions are the case where the second condition earns its keep. Words borrowed from English keep their English spelling, so "refund policy kya hai" still contains the literal words refund and policy - and the keyword half of the search often finds the passage on those alone. When the first search already worked, nothing is translated and nothing is spent.
The second condition is not an optimisation. A Ukrainian question ranked the right passage first as asked and second once translated, because відповідальність - liability - came back as "responsibility" rather than "penalty". Where the embedder already understands a language, replacing the user's own words only loses nuance. Adding this check took the result from 79/80 to 80/80 while making 31% fewer model calls.
How weak is weak enough is yours to set. Cross-lingual sensitivity in the project's Settings decides how badly the first search has to go before a translation is made - Auto (the default, tuned for most projects), Strict to translate rarely, Permissive to translate often, or a custom number from 0 to 1. Setting it to 0 switches the rewrite off entirely. There is no single correct value, because the same similarity score means different things on different embedding models, which is why the default is a named setting rather than a number you are asked to guess.
The setting is project-wide: it applies to /v1/query, /v1/retrieve, the
MCP server and the dashboard alike. It is not a per-request parameter,
because a translation spends your own provider key - what a caller may vary
per request is top_k, and nothing that costs a model call.
When a translation does run it is one small call, cached per question, metered and billed like any other. Identifying what language your documents are in happens once per project, not once per question. If anything fails, the question is searched exactly as typed - the behaviour Oreag had before this existed.
What is still weaker in other languages
-
Keyword matching stems in one language per project. Set it in Settings → Answer policy → Document language. Until you do, keyword search stems as English, which means a search only matches the exact form of a word the document happened to use - a Russian search for the singular will not find the plural. With the language set it will. Measured to rescue searches that fail today in Russian, German, Spanish, Portuguese, Italian, Dutch, Hindi, Nepali, Arabic, Indonesian, Greek, Hungarian, Serbian, Swedish, Catalan, Basque and Yiddish.
Changing it re-stems the index in place - one database update. Nothing is re-chunked, nothing is re-embedded, and your provider key is never touched. A project holding files in several languages should pick the one most of them are in; the setting is per project, not per file.
Languages Postgres has no stemmer for are deliberately absent from the list rather than offered as options that do nothing. For scripts written without spaces between words - Chinese, Japanese, Thai, Lao, Khmer, Burmese - Oreag indexes character by character, so keyword search finds things but matches more loosely. Meaning-based search is unaffected by any of this and carries the weight.
-
A project mixing writing systems - English and Hindi files together - is left alone, because the question's script is present in the corpus. Measured, a Hindi question still reached the English passage that answered it at rank 2 of 6, inside the window the model reads - so this is a ranking penalty, not a wall. Separate projects still rank it first.
-
Some languages your answer model writes badly. Asked to translate a sentence into Lao,
gpt-4o-miniproduced an 8,000-token blob. A weaker or more English-centric model will be worse in low-resource languages than any of the figures above suggest. -
Results depend on your embedding model. The numbers above are
text-embedding-3-large. A different embedding model - especially an English-only one - will behave differently.
Conversation memory (multi-turn follow-ups)
Query can remember a thread so you can ask natural follow-ups. Pass an optional conversation_id (any string, ≤ 128 chars) and Oreag loads the prior turns, rewrites a follow-up like "summarize that" into a standalone question before retrieval, answers it, and then saves the new turn (your question + the answer) back to the thread. The same conversation_id is echoed in the response. Threads are kept server-side for 24 hours. Omit conversation_id for a stateless, one-off query (the original behavior).
Streaming responses
Every answer surface can stream token by token over Server-Sent Events. POST /v1/projects/<project-id>/query/stream takes the same body as /query and emits data: frames: {"type":"token","text":...} as the answer is produced, a final {"type":"done","response":{...}} carrying the full payload (answer, sources, model, latency, cache_layer, cache_similarity), and {"type":"error","detail":...} on failure. Cache hits stream the stored answer the same way, so the client code path is identical. Every provider streams natively - OpenAI and OpenAI-compatible vendors, Anthropic, Gemini, Sarvam and local Ollama. The dashboard Playground uses this endpoint; the non-streaming /query remains for callers that want one JSON response.

Rate limits
/query, /query/stream and /retrieve share the standard budget: 120 requests/min per API key and 300 requests/min per project (all keys combined, fixed 60-second windows). Going over either returns 429 with a Retry-After header - wait that many seconds and retry. A 429 with Retry-After: 10 means the AI provider rate-limited the project's key instead; the fix there is provider quota, not fewer requests. Full tables live in Reference.
Repeated questions are cached (CAG) - and so are similar ones
Oreag caches answers in two layers, and every surface (the /v1 API, the dashboard Playground, and the MCP tools) goes through both:
- L1 - exact match (Redis / in-memory). If the same question (case/whitespace-insensitive) is asked against an unchanged project, the cached answer is served instantly with no retrieval and no LLM call. Simultaneous identical asks compute once.
- L2 - semantic match (pgvector). Different users rarely type the same words. Each answered question is stored with its embedding, and a new question is compared to them by cosine similarity - if the best match clears the threshold (default 0.75), the cached answer is reused; below it, the query runs for real. So "what is deep learning" can answer "explain deep learning to me" at the cost of one embedding call instead of a full retrieval + LLM run.
Both layers are scoped to the project, its embedding + LLM models, top_k, and a signature of the indexed content - uploading new files, saving memories, or changing models automatically invalidates them, so you never get a stale answer. Exact-match entries expire after 1 hour, semantic ones after 24 hours.
curl example
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/query" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "What is our refund window for enterprise customers?"}'JavaScript example
const res = await fetch(
"https://oreag.onrender.com/v1/projects/<project-id>/query",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
question: "What is our refund window for enterprise customers?",
top_k: 8,
}),
}
);
if (!res.ok) {
throw new Error(`Query failed: ${res.status}`);
}
const { answer, sources, model, latency_ms } = await res.json();
console.log(answer);
console.log(`Answered by ${model} in ${latency_ms} ms, ${sources.length} sources`);Errors
| Status | When |
|---|---|
401 | Missing/malformed bearer, or the key doesn't match this project (e.g. Invalid API key). |
404 | Project not found - no project with that id. |
409 | The project has neither indexed document chunks nor any embedded memories - there is nothing to ground an answer on. Upload and index documents (or save searchable memories) first. |
422 | Body validation failed (e.g. empty question, question over 4000 chars, or top_k outside 1–20). |
503 | The required embedding or LLM provider is unavailable (no usable key for the configured provider). |
Trying it without code
You can run the exact same pipeline from the dashboard before wiring up your app: open the project, go to the Playground tab, and ask a question. The Playground uses the identical retrieval + memory-blend path (authenticated with your dashboard session instead of an API key, and not written to the query log).
Monitor and improve answers
Use Queries for API history, Health for project diagnostics, and Answer feedback to collect ratings from your application. Playground tests the same query engine and project configuration.
Automatic quality checks
Open Evaluator → Automatic checks, save a test set, and select a 6-hour, 12-hour, daily, or weekly interval. Choose a completed reference run with exactly the same saved questions and configurations. Configure allowed pass-rate drop (percentage points), mean latency rise (%), and mean per-answer cost rise (%). With no reference, runs record metrics without regression warnings. Pass rate measures explicit expected-text/source rules, not factual correctness. Questions marked Review are excluded.

Schedule changes capture the currently saved set and do not automatically adopt later edits. Save the schedule again to adopt a changed set, then select a matching reference. Each scheduled run takes a new snapshot of the project's indexed knowledge. Live settings, vectors and query/Health metrics are unchanged. Provider usage remains metered; per-answer comparison cost includes measured query embedding and LLM usage, excluding snapshot preparation. A missing measurement is never treated as free or as zero latency. A zero baseline cannot support a relative percentage comparison and is skipped.
Warnings appear on saved runs and in the evaluation.regressed webhook. Thresholds trigger only when exceeded. Failed runs expose controlled errors; overlapping scheduled slots are skipped. The first run is one interval after saving. Missed slots do not build a backlog after downtime. Finished runs archive automatically as new runs arrive. Referenced and active runs stay protected; only a history filled entirely with protected/active runs blocks creation. Backend workers must be running; closing the browser does not affect them.
Owner-authenticated GET /api/projects/{project_id}/evaluations/schedule reads revision, enabled, interval_hours, reference_run_id, quality_limits, next_run_at, last_run_at and last_error. PUT accepts {revision, enabled, interval_hours, reference_run_id, quality_limits: {quality_drop_pp: 5, latency_increase_percent: 25, cost_increase_percent: 25}}. Schedule configuration is an owner action. Public API callers can queue individual background runs. Completed run responses and history include quality_report with state, metrics and warnings.

Archived evaluations
Choose Archived beside Saved runs to browse older results in pages of 20. Export remains available; archived runs are read-only and cannot resume or become a comparison reference. Public and owner run-list endpoints accept archived=true&offset=0; use offsets 20, 40 and so on for later pages. Archival changes only evaluation storage, never live project settings or production vectors.


Queries
Monitor the questions your application sends to Oreag. Queries lists recorded public /query and /query/stream calls alongside Playground tests. Each invocation, including exact and semantic cache hits, receives its own string query_id.

Dashboard workflow
Open Queries and use the compact search field beside the title. Open the filter icon next to it to filter by project, 7/30/90 days, cache result, minimum response time, or answer feedback, then choose Show results. Select a row to inspect the full question, recorded metrics, and feedback note. Newest records appear first. The first page refreshes every 30 seconds while visible and online; older pages keep their pagination position. Use Previous to return to earlier pages; automatic refresh resumes on the first page.
List questions are shortened to 320 characters and feedback notes are omitted from list previews. Detail returns the full question and note. Historical answers and source passages are not stored in these logs. Failed or interrupted requests without a completed log do not appear as completed queries.
Public API
Use a project key in Authorization: Bearer YOUR_API_KEY:
| Method and path | Response |
|---|---|
GET /v1/projects/<project-id>/queries | {items: QueryRecord[], next_cursor: string or null} |
GET /v1/projects/<project-id>/queries/<query-id> | One QueryRecord, including full question and feedback note |
| List parameter | Values / default |
|---|---|
days | 7, 30, or 90; default 30 |
search | Literal case-insensitive question substring, max 200 characters |
cache | all, fresh, l1, l2; default all |
feedback | all, helpful, not_helpful, unrated; default all |
min_latency_ms | Optional integer from 0 to 3,600,000 |
limit | 1–100; default 25 |
before | Positive bigint cursor; use the prior next_cursor unchanged |
// Server-side: never expose a project key in browser or mobile code.
const base = "https://oreag.onrender.com/v1/projects/YOUR_PROJECT_ID";
const headers = { Authorization: "Bearer YOUR_API_KEY" };
const response = await fetch(`${base}/queries?days=30&feedback=not_helpful&limit=25`, { headers });
if (!response.ok) throw new Error(await response.text());
const page = await response.json();
// Next page: add before=encodeURIComponent(page.next_cursor), if non-null.Keep IDs and cursors as strings: PostgreSQL bigints may exceed JavaScript's safe integer range. A record contains id, project_id, project_name, question, created_at, latency_ms, top_k, cache_layer, retrieval_similarity, cache_similarity, feedback_rating, feedback_note, and feedback_updated_at. Unmeasured metrics remain null.
Access and limits
Any active project key can read that project's history, including other keys' queries and Playground tests. These responses contain questions and feedback notes: authorize your own application's end users before sharing them. Owner sessions use GET /api/account/queries and GET /api/account/queries/<query-id> across owned projects; the owner list additionally accepts project_id.
Missing, invalid, revoked, or wrong-project keys return 401. Suspended projects/accounts return 403. Missing or out-of-project query details return 404. Invalid filters return 422. Reads use standard rate limits (120/min per key, 300/min per project by default), with Retry-After on 429.
Query debugging timeline
Open a query to see measured embedding, retrieval, translation, planning, and answer-generation stages. Timings are captured for both API and Playground queries, alongside existing tracing, without storing prompts or historical answers. Retrieval includes nested embedding/translation; overlapping durations should not be added together. Cache hits can have no provider stages. Older records say timing was not recorded. Timelines are returned only in query detail, including GET /v1/projects/{project_id}/queries/{query_id}; list responses omit them to stay small.


Health
Check whether the knowledge behind your application's API is ready to answer questions. Health includes files uploaded through the API and dashboard, query activity from regular and streaming API calls, Playground tests, and answer feedback.

Readiness and diagnostics
Search and the filter icon sit beside the Health title on desktop and mobile. Each project has a status dot at the top right: glowing red means needs attention, glowing yellow means queued, and glowing blue means indexing. Active indexing takes priority over queued files; file issues take priority over both. Ready projects have a steady green dot, paused projects amber, and projects without searchable files gray. The dot includes an accessible status label and tooltip; reduced-motion preferences disable pulsing. Select a project for file checks, query activity, feedback counts, and retrieval measurements.

Current files exclude superseded versions. Searchable files are indexed with at least one chunk. The report exposes queued_files (pending) and processing_files (actively indexing); indexing_files remains their combined total for compatibility. Duplicate counts represent extra current uploads with identical original-file hashes within a project. An indexing timestamp describes indexing time, not the freshness of the document's contents.
Query activity covers the last 30 days: total queries, cached queries, fresh queries, and the current helpful/not-helpful ratings on queries made in that period. Retrieval similarity averages only measured uncached queries. Missing measurements remain null, and unrated answers do not count as helpful. Readiness and similarity do not measure answer accuracy. The dashboard refreshes every 30 seconds while visible and online.
Public API
GET /v1/projects/<project-id>/health uses Authorization: Bearer YOUR_API_KEY. It returns the same diagnostic fields as the dashboard, scoped to the key's project:
{
"generated_at": "2026-09-10T09:00:00Z",
"query_window_days": 30,
"projects": [{
"id": "YOUR_PROJECT_ID",
"name": "Customer support",
"suspended": false,
"current_files": 12,
"searchable_files": 11,
"indexed_chunks": 284,
"failed_files": 1,
"review_files": 0,
"indexing_files": 0,
"empty_indexed_files": 0,
"unknown_files": 0,
"duplicate_copies": 0,
"last_indexed_at": "2026-09-10T08:00:00Z",
"total_queries": 128,
"cached_queries": 48,
"helpful_queries": 32,
"not_helpful_queries": 4,
"fresh_queries": 80,
"measured_queries": 76,
"avg_retrieval_similarity": 0.812
}]
}The projects array contains exactly one project. GET /api/account/knowledge-health requires an owner session and returns all owned projects. Public reads use standard rate limits, return 401 for invalid/revoked/wrong-project keys, 403 for suspended projects/accounts, and 429 with Retry-After when throttled. A suspended project can still be inspected by its owner through the dashboard.
Operational warnings appear above the project list when request failures, queue age or worker availability need attention. Open the linked Operations panel in Usage for details; the knowledge status of each project keeps its existing meaning.
Answer feedback
Collect ratings from users of your application through the public API. Playground provides thumbs-up/down controls for testing the same saved feedback; project owners review it in Queries and see counts in Health. No extra dashboard sidebar page is needed.

Submit, update, and remove
Read query_id from the normal query response, or response.query_id in the streaming terminal done event. Keep it as a string. If logging failed, the ID is null and feedback cannot be attached to that answer.
| Method and path | Result |
|---|---|
PUT /v1/projects/<project-id>/queries/<query-id>/feedback | Save or replace rating and note; 200 |
DELETE /v1/projects/<project-id>/queries/<query-id>/feedback | Remove rating and note; 204, empty body |
Both use a project API key. No upload permission is required. Any active key for the project can read or update feedback on its queries, including other keys' calls and Playground tests.
// Run on your server after an authorized user rates their answer.
const result = await fetch(
"https://oreag.onrender.com/v1/projects/YOUR_PROJECT_ID/queries/QUERY_ID/feedback",
{
method: "PUT",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
rating: "not_helpful", // or "helpful"
note: "The answer missed the cancellation policy.",
}),
}
);
if (!result.ok) throw new Error(await result.text());
console.log(await result.json());{
"query_id": "12345",
"rating": "not_helpful",
"note": "The answer missed the cancellation policy.",
"updated_at": "2026-09-10T09:00:00Z"
}rating is required and accepts only helpful or not_helpful. note is optional, trimmed, and limited to 1,000 characters. Omitted or blank notes clear the previous note. Null characters and unknown body fields are rejected. There is one mutable rating per query, not a vote per end user; the latest save replaces it. Repeating removal on an existing query is safe. Read the current rating and full note with the query-detail endpoint.
Review and test
In Playground, ask a question, choose Helpful or Not helpful under the answer, and optionally save a note. In Queries, use Answer feedback to filter ratings and open a record to review or edit existing feedback. In Health, open a project to inspect rating counts for queries made in the last 30 days.
Owner-session endpoints are PUT and DELETE /api/account/queries/<query-id>/feedback. Owner PUT returns the full updated query record; public PUT returns the compact response shown above. Both update the same stored rating. Feedback does not automatically retrain models or change retrieval, prompts, or cached answers. Each cache-hit invocation has its own query ID and feedback.
Missing/invalid/revoked/wrong-project keys return 401; suspension returns 403; missing/out-of-project queries return 404; invalid bodies return 422. Both operations share standard API rate limits and return 429 with Retry-After. Keep project keys server-side and check that a query belongs to the end user submitting feedback.
API reference in your project
Open the project's API tab for project-specific endpoint URLs and examples.

All screenshots in these sections use illustrative sample data. Apply migration 0043_answer_feedback.sql before deploying a backend that reads feedback columns; the read endpoints require no additional migration.
Turn feedback into a test
Open the query in Queries, select Add to test set, enter the answer you expected and optionally a source filename, then add it. This is useful for poorly rated answers: future evaluator runs can check the same question. Historical answer text is not retained in query logs, so the expected answer is entered by you.

POST /v1/projects/{project_id}/evaluations/cases/from-query accepts {query_id: "123", expected: "Return within 30 days.", source: "policy.pdf", match: "contains", revision: 2}. Read the current revision with GET evaluations/suite first. The owner route uses /api/projects/{project_id}/evaluations/cases/from-query. Query IDs remain strings in clients. Cross-project queries are rejected; duplicates and stale revisions return 409, and the set retains its 20-question limit. Adding a question never changes the live project or an existing run snapshot.
Retrieve (raw chunks, no LLM)
The retrieve endpoint runs only the retrieval half of RAG: it embeds your query and returns the most similar document chunks by cosine similarity. No LLM is called and no answer is generated. Use it when you want to:
- run your own model (or your own prompt) over the passages,
- build a custom UI that shows source snippets,
- power semantic search over the project's documents, or
- inspect exactly what context a
/querycall would retrieve.
Unlike /query, retrieve searches documents only - it does not blend in agent memories. (To search memories, use the MCP memory-search tools; to combine both, use /explore.)
Endpoint
| Method & path | POST /v1/projects/<project-id>/retrieve |
| Auth | Authorization: Bearer YOUR_API_KEY (project API key, oreag_sk_…) |
| Content-Type | application/json |
| Logged? | No - retrieve does not write a query log. |

Request body (RetrieveRequest)
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
query | string | yes | 1–4000 chars | The text to find similar chunks for. |
top_k | integer | no | 1–20 | Number of chunks to return. Defaults to 5 for this endpoint (note: this is a fixed default of 5, not the project's Top-K). |
{
"query": "data retention policy",
"top_k": 10
}Response body
Returns a JSON array of SourceChunk objects (not wrapped in an outer object), ordered by similarity descending:
| Field | Type | Description |
|---|---|---|
filename | string | The source file the chunk came from. |
page_number | integer | null | Page number for paginated sources (PDFs); null otherwise. |
chunk_index | integer | The chunk's position within its file. |
content | string | The chunk text. |
similarity | float | Cosine similarity (0–1) to the query. |
[
{
"filename": "security-handbook.pdf",
"page_number": 9,
"chunk_index": 22,
"content": "Customer data is retained for 90 days after account closure, then permanently deleted...",
"similarity": 0.79
},
{
"filename": "privacy-faq.md",
"page_number": null,
"chunk_index": 3,
"content": "We keep backups for up to 30 additional days for disaster recovery...",
"similarity": 0.64
}
]curl example
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/retrieve" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "data retention policy", "top_k": 10}'JavaScript example
const res = await fetch(
"https://oreag.onrender.com/v1/projects/<project-id>/retrieve",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "data retention policy", top_k: 10 }),
}
);
const chunks = await res.json();
for (const c of chunks) {
console.log(`${(c.similarity * 100).toFixed(0)}% ${c.filename}#${c.chunk_index}`);
}Errors
| Status | When |
|---|---|
401 | Missing/malformed bearer, or key doesn't match this project. |
404 | Project not found. |
422 | Body validation failed (empty query, over 4000 chars, or top_k outside 1–20). |
503 | The embedding provider is unavailable (no usable embedding key for the project's configured provider). |
Note: retrieve returns whatever chunks exist, even if the project is mid-indexing. If the project has no chunks yet, you'll get an empty array
[]rather than an error.
Agentic retrieval - explore the brain
The explore endpoint is Oreag's agentic-RAG entry point. Instead of returning a flat top-k list, it treats the project as a connected "brain": a graph whose nodes are both document chunks and agent memories, linked by related edges (semantic similarity). Explore seeds on the nodes nearest your query, then walks outward along those related links for a number of hops, and returns the resulting connected subgraph.
This lets a calling agent reason over how knowledge and memory connect - following a chain from a decision in memory to the document chunk that justifies it, or from one document's passage to a related passage in another file - rather than just reading the single best-matching snippet.
When to use explore vs. query
Use /query when… | Use /explore when… |
|---|---|
| You want a finished, written answer with citations. | You want the raw connected context to reason over yourself (typically an agent). |
| A simple chat-style Q&A is enough. | You need to follow how facts, decisions, and documents relate across the project. |
| You want documents + a light memory blend, summarized by an LLM. | You want both chunks and memories as first-class nodes, plus the edges between them. |
| One-shot retrieval suffices. | Multi-hop reasoning over the knowledge graph helps (e.g. "why was this decided, and what backs it up?"). |
Explore performs no LLM generation - it returns nodes (carrying their text) and edges. Your agent does the reasoning. It is the tool the MCP server's explore_brain action calls and is preferred over plain search_docs when you need to follow connections.
Endpoint
| Method & path | POST /v1/projects/<project-id>/explore |
| Auth | Authorization: Bearer YOUR_API_KEY (project API key, oreag_sk_…) |
| Content-Type | application/json |
Request body (BrainExploreRequest)
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
query | string | yes | 1–4000 chars | The query to seed the walk on. |
hops | integer | no | 0–3 | How many steps to expand outward from the seeds. 0 = seeds only (no expansion). Defaults to 1. |
{
"query": "why did we switch our default embedding model?",
"hops": 2
}How the walk works
- Seed. Oreag embeds the query and finds the nearest document chunks and nearest memories - up to 6 of each type (
explore_seeds_per_type = 6). - Expand. It runs a breadth-first walk for
hopssteps. At each frontier node it follows the strongestrelatedneighbours across all four directions - chunk→chunk, chunk→memory, memory→chunk, memory→memory - taking up to 4 neighbours per relation (explore_fanout = 4). Similarity is computed from each node's stored embedding by id, so expansion needs no extra vector round-trips. - Bound. The whole subgraph is capped at 50 nodes (
explore_max_nodes = 50). Edges are deduplicated (treated as undirected), typed"related", and carry similarity metadata.
Response body (BrainExploreResponse)
| Field | Type | Description |
|---|---|---|
query | string | Echo of the query you sent. |
seeds | array of string | The node ids the walk started from (the most relevant chunks and memories). |
nodes | array of MemoryGraphNode | Every node in the returned subgraph. |
edges | array of MemoryGraphEdge | The related edges connecting them. |
MemoryGraphNode
MemoryGraphNode:
| Field | Type | Description |
|---|---|---|
id | string | Stable node id (referenced by seeds and by edges). |
type | string | Node kind, e.g. "chunk" or "memory". |
label | string | Short human-readable label. |
text | string | null | The node's text content (chunk body or memory content). |
metadata | object | Extra fields (e.g. similarity, filename). |
MemoryGraphEdge
MemoryGraphEdge:
| Field | Type | Description |
|---|---|---|
source | string | Source node id. |
target | string | Target node id. |
type | string | Edge kind - "related" for explore. |
metadata | object | Edge metadata such as the similarity score. |
Example response
{
"query": "why did we switch our default embedding model?",
"seeds": ["memory:184", "chunk:9021"],
"nodes": [
{
"id": "memory:184",
"type": "memory",
"label": "Decision",
"text": "Decision: switch default embeddings to text-embedding-3-small for cost; re-index needed.",
"metadata": { "similarity": 0.71, "pinned": true }
},
{
"id": "chunk:9021",
"type": "chunk",
"label": "embedding-benchmarks.md",
"text": "In our benchmark, text-embedding-3-small matched recall at ~5x lower cost...",
"metadata": { "similarity": 0.66, "filename": "embedding-benchmarks.md" }
},
{
"id": "chunk:9044",
"type": "chunk",
"label": "embedding-benchmarks.md",
"text": "Dimensionality dropped from 3072 to 1536, halving vector storage...",
"metadata": { "similarity": 0.62, "filename": "embedding-benchmarks.md" }
}
],
"edges": [
{ "source": "memory:184", "target": "chunk:9021", "type": "related", "metadata": { "similarity": 0.64 } },
{ "source": "chunk:9021", "target": "chunk:9044", "type": "related", "metadata": { "similarity": 0.69 } }
]
}curl example
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/explore" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "why did we switch our default embedding model?", "hops": 2}'JavaScript example
const res = await fetch(
"https://oreag.onrender.com/v1/projects/<project-id>/explore",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
query: "why did we switch our default embedding model?",
hops: 2,
}),
}
);
const { seeds, nodes, edges } = await res.json();
console.log(`Started from ${seeds.length} seeds, walked to ${nodes.length} nodes / ${edges.length} edges`);
// Feed the connected subgraph to your own agent/LLM for multi-hop reasoning.
const context = nodes.map((n) => `[${n.id}] (${n.type}) ${n.text ?? n.label}`).join("\n");Errors
| Status | When |
|---|---|
401 | Missing/malformed bearer, or key doesn't match this project. |
404 | Project not found. |
422 | Body validation failed (empty query, over 4000 chars, or hops outside 0–3). |
503 | The embedding provider is unavailable (no usable embedding key for the project). Explore requires embeddings to seed and walk. |
Related: the full static graph
Explore returns a query-seeded subgraph. If you instead want the project's entire brain - every file, section, chunk, and memory with their structural and related edges - use the memory-graph endpoint:
curl "https://oreag.onrender.com/v1/projects/<project-id>/memory-graph" \
-H "Authorization: Bearer YOUR_API_KEY"
Where your API key comes from
All three endpoints above authenticate with a project API key (oreag_sk_…), created per project in the dashboard. These are distinct from your account-level provider keys (OpenAI/Gemini/etc.) - those are keys Oreag uses to call AI providers on your behalf, whereas a project API key is what your code uses to call Oreag. To create one, open the project and go to the API tab → Create key (the full value is shown only once).

Agent Memory
Oreag gives every project a persistent agent memory - a store of notes, decisions, and facts that your connected agents (typically via the Oreag MCP server) save and recall across sessions. Memory is one half of the project's "brain": a project's document chunks and its agent memories live in the same per-project embedding space (same provider, model, and dimension). Because both are embedded identically, a single cosine operator (pgvector <=>) can compare a memory against a document chunk directly.
That shared space is what lets memory:
- blend into RAG answers (the chat
/querypath mixes relevant memories into retrieved document context), - seed the brain explorer (
/explorewalks out from both chunks and memories), and - interlink with documents in the memory graph.
Memory exists so that an agent working in your codebase or workspace can write down what it learned - "we chose pgvector over Pinecone", "the client wants amber accents", "the staging DB URL is X" - and have a future session recall it semantically, not just by string match.
Endpoints at a glance
All memory endpoints are public (/v1) and authenticated with a project API key (oreag_sk_…) scoped to one project. The MCP server wraps these same routes as agent tools.
| Operation | Method & path | MCP tool | Request body / query | Response |
|---|---|---|---|---|
| Save a memory | POST /v1/projects/<project-id>/memory | save_memory | MemoryCreate | 201 MemoryOut (may include warning) |
| Semantic search | POST /v1/projects/<project-id>/memory/search | search_memory | MemorySearchRequest | list[MemorySearchResult] |
| Recent / pinned | GET /v1/projects/<project-id>/memory/recent?limit= | list_recent_memory | limit query (1–50, default 10) | list[MemoryOut] |
| Delete one | DELETE /v1/projects/<project-id>/memory/<memory-id> | delete_memory | - | 204 (MCP returns {deleted: <id>}) |
There is also an owner/dashboard surface (JWT-authenticated, used by the web UI Memory tab):
| Operation | Method & path | Response |
|---|---|---|
| List recent (owner view) | GET /api/projects/<project-id>/memory?limit= | list[MemoryOut] (limit clamped 1–500, default 100) |
| Delete (owner view) | DELETE /api/projects/<project-id>/memory/<memory-id> | 204 |
Why memory needs an embedding key
Memory is stored unconditionally but only searchable when it carries an embedding. When you save a memory, Oreag makes a best-effort attempt to embed the content using the project's embedding provider/model.
- If an embedding key is resolvable (a per-project override or your account-level provider key), the memory is embedded and becomes searchable.
- If no embedding key is resolvable, the memory is still saved - but with a
nullembedding - and the API returns awarning: "Stored without an embedding ... not searchable yet." It will not appear insearch_memoryresults or seed the brain explorer until the project gains an embedding key and the memory is re-embedded.
Endpoints that strictly require embeddings - POST /memory/search - return 503 when an embedding key is required but missing. (KEYLESS_PROVIDERS such as ollama and sentence_transformers need no key and never hit this 503.)
Recommendation: configure at least one embedding key before relying on semantic recall. See the navigation note below.

To set a key: Sidebar → API keys → Provider API keys → Add (account-wide), or open the project → Settings tab → Indexing & embedding card → key field (this project only).
Save a memory - POST /memory
MemoryCreate body:
| Field | Type | Default | Notes |
|---|---|---|---|
content | string | - | required, 1–8000 chars |
tags | string[] | [] | free-form labels |
pinned | boolean | false | pinned memories surface first in recall |
source | string | "mcp" | where the memory came from |
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/memory" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "We chose pgvector over Pinecone for the brain so chunks and memories share one cosine operator.",
"tags": ["architecture", "decision"],
"pinned": true
}'Successful response (201) - note the embedding field is never returned; only metadata is:
{
"id": 412,
"content": "We chose pgvector over Pinecone for the brain so chunks and memories share one cosine operator.",
"tags": ["architecture", "decision"],
"pinned": true,
"source": "mcp",
"created_at": "2026-06-21T09:14:02Z",
"updated_at": "2026-06-21T09:14:02Z"
}If the project has no embedding key, the same call still returns 201, but with a warning so your agent knows the memory is not yet searchable:
{
"id": 413,
"content": "Staging DB lives at db-staging.internal:5432.",
"tags": ["infra"],
"pinned": false,
"source": "mcp",
"created_at": "2026-06-21T09:15:40Z",
"updated_at": "2026-06-21T09:15:40Z",
"warning": "Stored without an embedding ... not searchable yet"
}Search memory - POST /memory/search
Cosine search over memories that have a non-null embedding. MemorySearchRequest:
| Field | Type | Default | Notes |
|---|---|---|---|
query | string | - | required, ≤4000 chars |
top_k | integer | 5 | number of results |
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/memory/search" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "query": "which vector store did we pick?", "top_k": 3 }'Response - each item is a MemoryOut plus a similarity score (1 = identical direction):
[
{
"id": 412,
"content": "We chose pgvector over Pinecone for the brain so chunks and memories share one cosine operator.",
"tags": ["architecture", "decision"],
"pinned": true,
"source": "mcp",
"created_at": "2026-06-21T09:14:02Z",
"updated_at": "2026-06-21T09:14:02Z",
"similarity": 0.83
}
]If the project has no embedding key, this endpoint returns:
{ "detail": "Embedding provider unavailable" }with HTTP status 503.
// JavaScript (fetch)
const res = await fetch(
`https://oreag.onrender.com/v1/projects/${projectId}/memory/search`,
{
method: "POST",
headers: {
Authorization: `Bearer ${YOUR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "which vector store did we pick?", top_k: 3 }),
}
);
if (res.status === 503) throw new Error("No embedding key configured for this project");
const results = await res.json();
for (const m of results) console.log(m.similarity.toFixed(2), m.content);Recent & pinned - GET /memory/recent
Returns recent memories with pinned entries first, then newest-first. Ideal for bootstrapping a new agent session before any specific query.
| Query param | Type | Default | Range |
|---|---|---|---|
limit | integer | 10 | clamped 1–50 |
curl "https://oreag.onrender.com/v1/projects/<project-id>/memory/recent?limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"[
{
"id": 412,
"content": "We chose pgvector over Pinecone ...",
"tags": ["architecture", "decision"],
"pinned": true,
"source": "mcp",
"created_at": "2026-06-21T09:14:02Z",
"updated_at": "2026-06-21T09:14:02Z"
},
{
"id": 410,
"content": "Client wants amber accent color on the landing hero.",
"tags": ["design"],
"pinned": false,
"source": "mcp",
"created_at": "2026-06-20T16:02:11Z",
"updated_at": "2026-06-20T16:02:11Z"
}
]Unlike
/memory/search,recentdoes not require an embedding key - it is a straight ordered read, so it returns memories whether or not they were embedded.
Delete a memory - DELETE /memory/<memory-id>
Deletes the memory if it belongs to the project (otherwise a 404-style miss). Returns 204 from the REST API; the MCP delete_memory tool returns { "deleted": <memory_id> }.
curl -X DELETE "https://oreag.onrender.com/v1/projects/<project-id>/memory/412" \
-H "Authorization: Bearer YOUR_API_KEY"How memory blends into RAG answers
When you call the chat /query endpoint (public /v1/query or the dashboard Playground), Oreag does more than retrieve document chunks. If memory blending is enabled (it is by default), the query pipeline also searches up to 4 memories (rag_memory_blend_k) and appends any whose similarity is ≥ 0.35 (rag_memory_min_similarity) as pseudo-sources - they appear in the answer's sources array with filename: "memory" and chunk_index: -1. All sources (document + memory) are then sorted by similarity before the LLM writes the grounded answer.
If the project has no embedding key, memory blending is silently skipped (the document-only RAG path still runs). This means a useful pattern is: have your agent save_memory important decisions, and they will automatically start informing future /query answers - no extra wiring.
Note: a chat
/queryreturns 409 if the project has neither document chunks nor any embedded memories - there is simply nothing to ground an answer on yet.
The Memory tab (dashboard)
Inside a project, the Memory tab is the owner-facing view of everything your agents have saved:
- A card titled "Agent memory" - "Notes your connected agents (via the MCP server) have saved for this project."
- A filter input (search icon) that filters the list by
content(case-insensitive). It is disabled when there are no memories. - States: error ("Could not load memories: …"), loading (three skeletons), empty (brain icon + "No memories yet"), and filtered-empty ("No memories match '…'.").
- Memory rows show the content text plus meta badges: a pinned badge when pinned, each tag as an outline badge, the source, and the created date. Each row has a ghost Delete button that issues
DELETE /api/projects/<project-id>/memory/<memory-id>.
The Memory tab is read-and-delete only in the UI - creation is done by agents through the MCP//v1 surface, which is the intended authoring path.

Typical agent session
- Bootstrap: call
list_recent_memoryto load pinned + recent notes and orient the session. - Work: use
search_memory(andsearch_docs/explore_brain) to recall relevant context on demand. - Record:
save_memorynew decisions and facts so the next session - and future RAG answers - inherit them.
# 1. Orient
curl "https://oreag.onrender.com/v1/projects/<project-id>/memory/recent?limit=10" \
-H "Authorization: Bearer YOUR_API_KEY"
# 2. Recall during the task
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/memory/search" \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{ "query": "auth approach", "top_k": 5 }'
# 3. Record a new decision
curl -X POST "https://oreag.onrender.com/v1/projects/<project-id>/memory" \
-H "Authorization: Bearer YOUR_API_KEY" -H "Content-Type: application/json" \
-d '{ "content": "Switched JWT verification to JWKS (ES256).", "tags": ["auth"], "pinned": false }'Memory Graph
The memory graph is the full, static map of a project's brain - every document and every agent memory rendered as one interconnected graph. Where agent memory is the session knowledge and document chunks are the reference knowledge, the memory graph is where the two are tied together explicitly: document chunks and saved memories are nodes in the same graph, joined by related edges computed from cosine similarity in the shared per-project embedding space.
This is the literal expression of the brain concept: documents + session memory in one structure, so you can see (and traverse) how a memory an agent saved relates to the passages in your uploaded files - and how files relate to each other.
Endpoint
| Surface | Method & path | Auth | MCP tool | Response |
|---|---|---|---|---|
| Public | GET /v1/projects/<project-id>/memory-graph | API key (oreag_sk_…) | get_memory_graph | MemoryGraphResponse |
| Owner | GET /api/projects/<project-id>/memory-graph | Supabase JWT | - | MemoryGraphResponse |
Both surfaces call the same build_memory_graph builder and return an identical shape.
curl "https://oreag.onrender.com/v1/projects/<project-id>/memory-graph" \
-H "Authorization: Bearer YOUR_API_KEY"Node types
The graph contains five node kinds. The first four describe the document side of the brain (its structure, derived from your files); the last is the memory side.
Node type | What it represents | Notes |
|---|---|---|
project | The project root | Exactly one per graph |
file | One uploaded file | Rich metadata, including markdown_available |
section | A heading-bounded section | Parsed from the file's converted markdown headings |
chunk | One embedded text chunk | The document half of the brain; positioned into its section |
memory | One saved agent memory | The memory half of the brain; same embedding space as chunks |
Edge types
Edges are either structural (how the document decomposes) or semantic (similarity links computed from embeddings).
Structural edges
Edge type | From → To | Meaning |
|---|---|---|
contains | project → file | The project owns the file |
contains | file → chunk / section → chunk | The file (or section) contains the chunk |
contains | project → memory | The project owns the memory |
derived_from | chunk → file | The chunk was produced from that file |
next | chunk → chunk | Sequential reading order within a file |
Semantic (related) edges
related edges connect nodes that are close in the embedding space. They carry similarity metadata and are undirected (deduplicated). This is the layer that fuses memory with documents.
related link | How it's built | Defaults |
|---|---|---|
| chunk ↔ chunk (cross-file) | RELATED_SQL cross-file similarity, oriented newer→older | top-5 per chunk, threshold 0.6, max 600 edges; skipped if chunk_count < 2 or > 1500 chunks |
| file ↔ file | Aggregated from cross-file chunk relations | carries shared_topics and avg_similarity |
| memory → chunk | MEMORY_CHUNK_SQL cosine | top-5, threshold 0.6 |
| memory ↔ memory | MEMORY_MEMORY_SQL cosine | top-5, threshold 0.6, max 400 edges; skipped above 1000 memories |
Semantic edges require embeddings. Memories saved without an embedding (no embedding key at save time) have no
relatededges - they still appear asmemorynodes connected by the structuralproject → memorycontainsedge, but they will not link to chunks or other memories until embedded.
How memories interlink with chunks
Because a chunk's embedding and a memory's embedding live in the same vector space, the builder can compute memory → chunk similarity directly. The practical effect:
- A memory like "we standardized on amber accents" will grow a
relatededge to the chunk(s) of your brand-guidelines PDF that discuss color - even though nothing textually links them. - Memory ↔ memory edges cluster related decisions together.
- File ↔ file edges (aggregated from chunk relations) reveal which documents cover overlapping topics (
shared_topics).
This is also what powers the agentic explore_brain traversal: it seeds on nearby chunks and memories, then walks these same related links outward up to hops (0–3) steps. The memory graph is the static, whole-brain view; explore is the focused, query-driven subgraph over the same edges.
Sample response
MemoryGraphResponse has three top-level keys: project, nodes, and edges. (Shape abbreviated for illustration; exact node metadata fields are governed by MemoryGraphNode.)
{
"project": {
"id": "<project-id>",
"name": "Brand Knowledge Base",
"status": "ready"
},
"nodes": [
{ "id": "project", "type": "project", "label": "Brand Knowledge Base" },
{ "id": "file:9af3", "type": "file",
"label": "brand-guidelines.pdf",
"markdown_available": true, "chunk_count": 12, "page_count": 8 },
{ "id": "section:9af3-2", "type": "section", "label": "Color System" },
{ "id": "chunk:1187", "type": "chunk",
"label": "Primary accent is amber (#F59E0B) ...",
"file_id": "9af3", "chunk_index": 5, "page_number": 3 },
{ "id": "chunk:1188", "type": "chunk",
"label": "Secondary palette uses sky blue ...",
"file_id": "9af3", "chunk_index": 6, "page_number": 3 },
{ "id": "memory:412", "type": "memory",
"label": "Client wants amber accent color on the landing hero.",
"tags": ["design"], "pinned": false }
],
"edges": [
{ "source": "project", "target": "file:9af3", "type": "contains" },
{ "source": "file:9af3", "target": "chunk:1187", "type": "contains" },
{ "source": "section:9af3-2","target": "chunk:1187", "type": "contains" },
{ "source": "chunk:1187", "target": "file:9af3", "type": "derived_from" },
{ "source": "chunk:1187", "target": "chunk:1188", "type": "next" },
{ "source": "project", "target": "memory:412", "type": "contains" },
{ "source": "chunk:1187", "target": "chunk:1188", "type": "related",
"similarity": 0.74 },
{ "source": "memory:412", "target": "chunk:1187", "type": "related",
"similarity": 0.81 }
]
}The two related edges at the end are the brain in action: the agent's memory (memory:412) is linked to the relevant document chunk (chunk:1187) purely through embedding similarity (0.81), and two chunks across the document are linked to each other (0.74).
How to fetch the graph
# curl (public API key)
curl "https://oreag.onrender.com/v1/projects/<project-id>/memory-graph" \
-H "Authorization: Bearer YOUR_API_KEY"// JavaScript (fetch)
const res = await fetch(
`https://oreag.onrender.com/v1/projects/${projectId}/memory-graph`,
{ headers: { Authorization: `Bearer ${YOUR_API_KEY}` } }
);
const graph = await res.json();
console.log(`${graph.nodes.length} nodes, ${graph.edges.length} edges`);The API tab of a project exposes this endpoint as a copy-paste snippet ("Agent memory graph") so you can grab the exact URL with the correct host:


Agents reach the same data through the MCP get_memory_graph tool (no parameters), which calls GET /v1/projects/<id>/memory-graph under the hood.
How to visualize the graph
The response is a generic { nodes, edges } graph, so it drops straight into any graph-rendering library. Map each node type to a color/shape and each edge type to a line style.
// Example: shape a memory-graph response for a force-directed renderer (e.g. D3, vis-network, Cytoscape)
const graph = await fetchMemoryGraph(projectId); // GET /v1/.../memory-graph
const colorByType = {
project: "#6b7280", // gray
file: "#0ea5e9", // sky
section: "#8b5cf6", // violet
chunk: "#10b981", // emerald
memory: "#f59e0b", // amber - the memory half of the brain
};
const nodes = graph.nodes.map((n) => ({
id: n.id,
label: n.label,
color: colorByType[n.type] ?? "#999",
group: n.type,
}));
const edges = graph.edges.map((e) => ({
from: e.source,
to: e.target,
// structural edges solid; semantic 'related' edges dashed + weighted by similarity
dashes: e.type === "related",
value: e.similarity ?? 1,
title: e.type === "related" ? `related (${e.similarity?.toFixed(2)})` : e.type,
}));
// hand { nodes, edges } to your graph library of choiceVisualization tips:
- Color by node type so the document side (project/file/section/chunk) and the memory side stand out - amber
memorynodes against emeraldchunknodes make the brain's two halves legible at a glance. - Distinguish edge types: render structural edges (
contains,derived_from,next) as solid lines and semanticrelatededges as dashed lines, weighting their thickness bysimilarity. Therelatedlines crossing from memories into document chunks are the most insightful part of the picture. - Use a force-directed layout so semantically related nodes cluster together; topically overlapping files (joined by file↔file
relatededges carryingshared_topics) will visibly group. - Scale gracefully: on large projects the builder caps semantic edges (chunk relations skipped above 1500 chunks; memory relations above 1000 memories), so the graph stays renderable; structural edges are always present.
Oreag MCP Server
The Oreag MCP server (oreag-mcp v0.2.0) gives coding agents - Claude Code, Codex, claude.ai/Claude Desktop - per-project memory plus RAG over a project's documents, all scoped by a single Oreag project API key (oreag_sk_…). It is built on FastMCP (the mcp[cli] SDK) and can run two ways:
- Locally over stdio - the client launches it as a subprocess (one project per repo).
- Remotely over streamable-HTTP - a deployed connector that is multi-tenant: each caller supplies their own project id (in the URL) and their own project API key (as a bearer token).
Every tool delegates to an OreagClient HTTP wrapper that calls /v1/projects/<project-id><suffix> with Authorization: Bearer <api_key> and a 60-second timeout.
The 9 tools
All tools below are registered in server.py. The first six are the documented core; the last three (add_document, get_memory_graph, explore_brain) also exist and are fully usable.
| Tool | Parameters (defaults) | Purpose | Backend call |
|---|---|---|---|
save_memory | content: str, tags: list[str] | None = None, pinned: bool = False | Save a project memory (decision, fact, or note) for future sessions. | POST /v1/projects/<id>/memory body {content, tags (or []), pinned} |
search_memory | query: str, limit: int = 5 | Recall the most relevant saved memories for the current task. | POST /v1/projects/<id>/memory/search body {query, top_k: limit} |
list_recent_memory | limit: int = 10 | List recent + pinned memories to orient a new session. | GET /v1/projects/<id>/memory/recent?limit= |
delete_memory | memory_id: int | Delete a memory entry by id. | DELETE /v1/projects/<id>/memory/<memory_id> → {deleted: memory_id} |
search_docs | query: str, top_k: int = 5 | Search the project's uploaded documents for relevant passages (retrieval only, no LLM). | POST /v1/projects/<id>/retrieve body {query, top_k} |
ask_docs | question: str | Ask a question and get a grounded RAG answer from the project's documents. | POST /v1/projects/<id>/query body {question} |
add_document | filename: str, content: str | Upload a text document so it is chunked, embedded, and searchable. Requires an upload-enabled key (read-only keys get 403); any text content is accepted regardless of extension. | POST /v1/projects/<id>/files multipart uploads=(filename, content, text/plain) |
get_memory_graph | (none) | Fetch the project's interlinked "brain": a graph whose nodes are document chunks AND saved memories, joined by related (semantic-similarity) edges. | GET /v1/projects/<id>/memory-graph |
explore_brain | query: str, hops: int = 1 | Agentic retrieval over the brain. Seeds on the most-relevant chunks + memories, then expands hops (0–3) steps along related links, returning a connected subgraph. Prefer over search_docs when you need to follow how knowledge and memory connect. | POST /v1/projects/<id>/explore body {query, hops} |
Typical agent session
- Bootstrap with
list_recent_memoryto recall pinned decisions and recent notes. - While working, use
search_docs/search_memory(orexplore_brainfor multi-hop context). - Call
save_memoryto record new decisions for the next session.
Connect locally (uvx / stdio)
In stdio mode the client runs the server as a subprocess. Credentials come from environment variables: OREAG_API_KEY and OREAG_PROJECT_ID (both required), optionally OREAG_API_BASE.
Claude Code (single project per repo)
claude mcp add oreag -- uvx --from /path/to/mcp-server oreag-mcp \
-e OREAG_API_KEY=oreag_sk_xxx -e OREAG_PROJECT_ID=<project-id>Or declare it in .mcp.json (on Windows, set args to ["--from", ".\\mcp-server", "oreag-mcp"]):
{
"mcpServers": {
"oreag": {
"command": "uvx",
"args": ["--from", "./mcp-server", "oreag-mcp"],
"env": {
"OREAG_API_KEY": "oreag_sk_xxx",
"OREAG_PROJECT_ID": "<project-id>",
"OREAG_API_BASE": "https://oreag.onrender.com"
}
}
}
}Codex (~/.codex/config.toml) - local
Codex supports remote URLs (below). For a local stdio launch, point it at uvx the same way and supply the env vars.
Connect remotely (multi-tenant HTTP)
A deployed server in HTTP mode exposes a per-project connector at:
https://<host>/projects/<project-id>/mcp
The project id lives in the URL path; the project API key is sent as a bearer token. The server stores no project secrets in multi-tenant mode - each request is self-contained (stateless_http=True).

Claude Code (remote)
claude mcp add --transport http oreag \
https://<host>/projects/<project-id>/mcp \
--header "Authorization: Bearer YOUR_API_KEY"claude.ai / Claude Desktop
Connectors → Add custom connector → paste https://<host>/projects/<project-id>/mcp and supply Authorization: Bearer YOUR_API_KEY via the client's header/OAuth field.
Codex (~/.codex/config.toml) - remote
[mcp_servers.oreag]
url = "https://<host>/projects/<project-id>/mcp"
http_headers = { Authorization = "Bearer YOUR_API_KEY" }Each user supplies their own project id (URL) and their own project API key (bearer), keeping accounts isolated - isolation is enforced by the backend. Exact remote-server flag names shift across client versions; check each client's docs.
A missing bearer on the multi-tenant route returns 401 {"error":"missing Authorization: Bearer <project-api-key>"}.
Transports & request routing
The transport is chosen by the MCP_TRANSPORT env var (lowercased; _→-):
MCP_TRANSPORT | Behavior |
|---|---|
stdio (default / any unrecognized value) | mcp.run() - local subprocess. |
http or streamable-http | Runs uvicorn serving the streamable-HTTP app behind the HTTP gate. |
sse | SSE transport (present in code, undocumented in README). |
The HTTP gate routes by path:
| Route | Mode | Project + key source |
|---|---|---|
GET /health | Health check | Unauthenticated; returns 200 "ok". |
ANY /projects/<id>/mcp | Multi-tenant | Project id from URL; key from caller's Authorization: Bearer. Missing bearer → 401. |
ANY /mcp (any other path) | Single-project | Server's OREAG_PROJECT_ID + OREAG_API_KEY env. If MCP_AUTH_TOKEN is set and the request bearer ≠ token → 401 {"error":"unauthorized"}. |
Environment variables
| Var | Default | Role |
|---|---|---|
OREAG_API_BASE | https://oreag.onrender.com | Backend base URL. In multi-tenant mode this is the only secret needed. |
OREAG_API_KEY | (required: stdio / single-project) | Project API key oreag_sk_…. Not used in multi-tenant (comes from caller's bearer). |
OREAG_PROJECT_ID | (required: stdio / single-project) | Project UUID. Not used in multi-tenant (comes from URL path). |
MCP_TRANSPORT | stdio | stdio / http / streamable-http / sse. |
MCP_AUTH_TOKEN | "" (unset = no guard) | Optional shared-secret bearer guarding the single-project /mcp URL. Recommended for single-project HTTP deploys. |
HOST | 0.0.0.0 | Bind host. Usually injected by the platform. |
PORT | 8000 | Bind port. Injected by most platforms ($PORT). |
Requirement matrix:
| Var | Multi-tenant | Single-project |
|---|---|---|
MCP_TRANSPORT=http | required | required |
OREAG_API_BASE | required | required |
OREAG_PROJECT_ID | - (from URL) | required |
OREAG_API_KEY | - (from caller) | required |
MCP_AUTH_TOKEN | - | recommended |
PORT / HOST | injected | injected |
To set these environment variables in a shell before launching the server:
export MCP_TRANSPORT=http
export OREAG_API_BASE=https://your-backend.onrender.com
export OREAG_API_KEY=oreag_sk_xxx
export OREAG_PROJECT_ID=<project-id>Deploy
Docker
The image is python:3.12-slim, installs uv, builds the package with hatchling, and bakes MCP_TRANSPORT=http HOST=0.0.0.0 PORT=8000, EXPOSE 8000, CMD ["oreag-mcp"]. Runtime credentials are not baked in - set them as platform env/secrets.
docker build -t oreag-mcp .
docker run -p 8000:8000 \
-e MCP_TRANSPORT=http \
-e OREAG_API_BASE=https://your-backend.onrender.com \
oreag-mcp
# multi-tenant URL -> http://localhost:8000/projects/<project-id>/mcpRender (render.yaml)
One web service, runtime: docker, plan: free, healthCheckPath: /health. MCP_TRANSPORT=http is literal; OREAG_API_BASE has sync: false (set in the dashboard, kept out of git). Single-project vars are commented-out templates with sync: false. Serves the multi-tenant connector at https://<service>.onrender.com/projects/<project-id>/mcp.
Railway / Heroku-style (Procfile)
web: env MCP_TRANSPORT=http oreag-mcp
Fly.io
fly launch
fly secrets set OREAG_API_BASE=https://your-backend.onrender.com
fly deployFor multi-tenant deploys the server holds no project keys - set only MCP_TRANSPORT=http and OREAG_API_BASE.
Health check
GET /health → 200 body ok (text/plain), unauthenticated, handled before any routing. Used as the platform health check.
Project API Keys
Project API keys are bearer tokens that let outside callers - your app, a script, a coding agent, or an MCP client - hit a specific project's /v1 REST and MCP endpoints (query, retrieve, upload, memory, explore, memory-graph). They authenticate into Oreag and are not provider keys. Each key is created and owned per project, and its full value (oreag_sk_…) is shown only once at creation.
Under the hood, only a SHA-256 hash of the key is stored, alongside a display prefix (the first 16 characters). The format is oreag_sk_ + a URL-safe random token.
Where to manage keys
- Open the project from the sidebar (
/projects/<project-id>). - In the top tab bar, click the API tab (between Playground and Settings).
- The first card is "API keys" (subtitle: "Keys are shown once at creation - store them securely.") with a Create key button in its top-right corner.
The key table has columns: Key (<prefix>…, mono), Access (an "Uploads" checkbox), Created, Last used (datetime or "Never"), Status (green Active / grey Revoked badge), and a ⋯ actions menu. Active keys sort to the top (newest first); revoked keys sink to the bottom. With no keys, the table shows "No keys yet - create one to call your RAG API."

Create a key
- Click Create key (top-right of the card; shows a loader while creating). This POSTs to
/api/projects/<project-id>/keyswith name"default". - A dialog "API key created" appears containing the full key in a copy field, with the warning: Copy this key now - for security it will never be shown again.
- Copy it, then close the dialog. The new key appears in the table as Active.

Security: the plaintext key is shown exactly once. Oreag stores only its hash - there is no way to retrieve the full value later. If you lose it, create a new key and revoke the old one.
Use the key as a bearer token against the project's /v1 endpoints:
curl -X POST https://oreag.onrender.com/v1/projects/<project-id>/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "What is our refund policy?"}'const res = await fetch(
"https://oreag.onrender.com/v1/projects/<project-id>/query",
{
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ question: "What is our refund policy?" }),
}
);
const { answer, sources } = await res.json();The Uploads permission (read vs. read+write)
Every key carries a per-key can_upload flag (default false - keys are read-only unless you grant uploads).
- In the table's Access column, each row has an "Uploads" checkbox. Unchecked = read-only (query/retrieve/memory only); checked = the key may also upload/ingest files via
POST /v1/projects/<id>/files. - Click the checkbox to flip it. The change is optimistic - the row updates instantly, then PATCHes
{can_upload}; it reverts and toasts on error. - The checkbox is disabled for revoked keys.
- A read-only key that calls the upload endpoint gets 403 (read-only). The "Upload documents" snippet in the endpoints card notes this.
# Requires a key with Uploads enabled, else 403
curl -X POST https://oreag.onrender.com/v1/projects/<project-id>/files \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "uploads=@./handbook.pdf"
Revoke vs. Delete
Both immediately break any caller using the key. They differ in what remains visible.
Revoke (soft)
- On the key's row, open the ⋯ actions menu (labeled "<prefix> actions").
- Click Revoke (only shown for non-revoked keys).
- Confirm in the "Revoke this API key?" dialog (warns callers will get 401s and it cannot be undone). Click Revoke key.
- A "Revoking key…" loader plays, then the Status badge flips to grey Revoked and the row sinks to the bottom.
A revoked key stays in the table for audit but no longer works. Revoke sets revoked_at; the backend will reject the key with 401 "Invalid API key".
Delete (hard purge)
- Open the same ⋯ menu (Delete is available for both active and already-revoked keys).
- Click Delete (red/destructive item).
- Confirm in the "Delete this API key?" dialog (warns the row is permanently removed and any app/agent using it stops immediately). Click Delete.
- A "Permanently deleting…" loader plays, then the row disappears entirely. This DELETEs
/keys/<key-id>/purge.
| Action | Endpoint | Row after | Key works after |
|---|---|---|---|
| Revoke | DELETE /api/projects/<id>/keys/<key-id> | Stays (grey Revoked) | No |
| Delete | DELETE /api/projects/<id>/keys/<key-id>/purge | Removed entirely | No |
Account-Level vs. Project-Level Keys (Migration Guide)
Oreag has three distinct kinds of keys. They live on different screens and do completely different things - mixing them up is the most common source of confusion. This guide states what each key is, exactly where to manage it, and the precise click-path.
The three concepts at a glance
| # | Concept | What it is | Where to manage it | Looks like |
|---|---|---|---|---|
| 1 | Account-level provider keys | Your OpenAI / Gemini / Anthropic / Sarvam keys. One per provider, shared by every project on your account. | Sidebar → API keys → "Provider API keys" card | sk-…, shown as ••••••••<last4> |
| 2 | Project-level key overrides | A single project choosing to use its own provider key for its embedding model and/or its answer (LLM) model. | Open the project → Settings tab → the key field under Answer model and/or Indexing & embedding | Project key ••••<last4> |
| 3 | Project API keys | Bearer tokens external apps / agents / MCP clients use to call this project's /v1 + MCP endpoints. | Open the project → API tab → Create key | oreag_sk_…, full value shown once |
Key mental model: Concepts 1 and 2 are keys Oreag uses to call the AI providers on your behalf. Concept 3 is a key other programs use to call Oreag. They are never interchangeable.

1. Account-level provider keys (shared across all projects)
What it is: Your own API key for a model provider. Oreag encrypts it at rest (Fernet) and uses it for embedding and answering across all your projects, unless a specific project overrides it (concept 2). One key per owner + provider; only the last 4 characters are kept for display.
Supported providers:
- OpenAI - Embeddings + chat (
sk-…) - Google Gemini - Embeddings + chat
- Anthropic (Claude) - Chat only
- Sarvam AI - Chat only (Indic LLMs)
(If you would rather not paste any key, run a local Ollama model instead - it needs no key.)
Get to the screen
- In the left sidebar, click API keys (key icon - third item after Overview and New project). This navigates to
/settings/api-keys. - The page title reads "API keys", subtitle "Your own provider keys (OpenAI, Gemini, Anthropic), shared across all your projects."
- The first card is "Provider API keys" - this screen.


Add (or replace) a provider key
- On the provider's row, click Add (reads Replace if a key is already stored).
- A dialog "<Provider> API key" opens with a password-masked input and a note that pasted keys are encrypted and only the last 4 characters are kept.
- Paste the key. (Pressing Enter in the field also saves.)
- Click Save key. On success you get a "Key saved" toast, the dialog closes, and the row shows
••••••••<last4>. This PUTs/api/provider-keysand revalidates/api/modelsso model pickers refresh.

Remove a provider key
- On a row that has a key, click Remove (ghost button, only when a key exists).
- Confirm in "Remove your <Provider> key?" - it warns projects relying on this account key (with no key of their own) will stop embedding/answering.
- Click Remove key (destructive). A "Removing key…" loader plays, then the row reverts to Not set. This DELETEs
/api/provider-keys/<provider>.

Effect: Adding a key makes that provider's models selectable in every project's model pickers (availability is per account). Removing it breaks any project relying on it without its own override.
2. Project-level key overrides (one project, its own provider key)
What it is: A single project can use its own provider key for its embedding model and/or its answer (LLM) model, instead of the shared account key. Set inside the project's Settings - the account page only lists these overrides. There are two independent override slots per project: the Answer (LLM) key and the Embedding key.
Key resolution precedence is: per-project override → account provider key → none (with ollama / sentence_transformers needing no key).
Get to the screen (two ways)
A. From the project directly
- In the sidebar Projects list, click the project (
/projects/<project-id>). - In the tab bar (Files / Memory / Playground / API / Settings), click Settings.
B. From the account API-keys page
- Sidebar → API keys → scroll to the second card, "Project key overrides."
- On the project's row, click Manage (label swaps to a spinner while navigating). This deep-links to
/projects/<project-id>?tab=settings.

Where the controls are (Settings tab)
Two cards carry override fields:
- "Answer model (LLM)" - a model picker plus the LLM key field.
- "Indexing & embedding" - chunk size/overlap, an embedding-model picker, plus the embedding key field.
Each key field (ProviderKeyField) has three visual states:
- Account key in use, no override: plain text "Using your account
<provider>key." + an underlined link "Use a project key instead." - Override stored: a masked chip
Project key ••••<last4>with Replace, plus either "Use account key" (when an account key exists) or "Remove" (when none does). - No key anywhere for that provider: a password input prompting you to paste a key for this project.



Set a project override
Answer (LLM) key:
- In the "Answer model (LLM)" card, click "Use a project key instead" (or Replace if a key is stored). A password input appears.
- Paste the project's provider key.
- Click Save (Cancel sits next to it). On success: "Project key saved" toast, and the field collapses to the
Project key ••••<last4>chip.
Embedding key - behavior depends on whether you also changed the model/chunking:
- In "Indexing & embedding," reveal the input the same way and paste the key.
- If you changed only the key (same model + chunk settings), click Save - instant, no re-index.
- If you also changed the embedding model or chunk size/overlap, the button reads "Change & re-index". Clicking it opens a "Re-index all files?" confirmation (every file is re-chunked/re-embedded); confirm with Re-index to apply the new key + model together.


Revert to the account key
With an override stored, the chip offers one of two buttons:
- "Use account key" (shown when the account has a key for that provider): click it - the override drops immediately, toast "Reverted to account key". The field returns to the "Using your account key" state.
- "Remove" (shown when there is no account key to fall back on): click it, then confirm in "Remove this project key?" (warns the project can't use that provider until a new key is added). Confirm with Delete.

Note - switching providers: If you change the model picker to a different provider without entering a new key, any override belonging to the old provider is dropped automatically and resolution falls back to the new provider's account key.
Where it shows up afterward
Back on Sidebar → API keys → "Project key overrides", the project appears as a row with Embedding key and Answer (LLM) key columns - each cell shows ••••••••<last4> where an override exists, or muted "account key" where it still uses the account key. A project with no overrides does not appear; when none of your projects have overrides the card shows "No project-level keys - every project uses your account keys above." The list updates immediately (no manual refresh).

3. Project API keys (oreag_sk_… for external apps / MCP)
What it is: Bearer tokens that let outside callers hit this project's REST/MCP endpoints. These authenticate into Oreag (not into a provider). Created per project; full value (oreag_sk_…) shown only once. Full details - creation, the Uploads permission, revoke vs. delete - are in the Project API Keys section.
Get to the screen
- Open the project from the sidebar (
/projects/<project-id>). - Click the API tab (between Playground and Settings).
- The first card is "API keys" with a Create key button top-right; below it the "Your RAG endpoint" card lists copy-paste URLs and curl/JS/MCP snippets.

Quick decision guide
- "I want to plug in my OpenAI / Gemini / Anthropic / Sarvam key for everything" → Concept 1: Sidebar → API keys → Provider API keys → Add.
- "I want this one project to use a different provider key for its embeddings or its answer model" → Concept 2: open the project → Settings tab → the key field in the Answer model or Indexing & embedding card.
- "I want my app / agent / MCP client to call this project's API" → Concept 3: open the project → API tab → Create key.

Reference
A compact reference for every public /v1 endpoint, every MCP tool, and the platform limits and quotas.
Authentication
All /v1 endpoints take a project API key as a bearer token. The key is scoped to exactly one project (it already identifies the project), so there is no separate ownership check beyond a 404 if the project id doesn't exist.
Authorization: Bearer oreag_sk_...
- Missing / malformed bearer, or one not starting with
oreag_sk_→ 401. - No hash match for the project (or revoked) → 401 "Invalid API key".
A public, no-auth health endpoint also exists: GET /healthz → {"status":"ok"}.
Public /v1 endpoints
All paths are prefixed /v1/projects/<project-id>.
| Method & path | Body / params | Purpose | Response |
|---|---|---|---|
GET / | - | Lightweight project info. | ProjectInfo (id, name, status, file_count) |
POST /query | {question (1–4000), top_k? (1–20), conversation_id? (≤128)} | Grounded RAG answer; blends in relevant memories; logs to query_logs. | QueryResponse (query_id, answer, sources, model, latency_ms, depth, sub_queries, needs_clarification, clarification_questions, conversation_id) |
POST /query/stream | Same body as /query | SSE events, terminal done.response includes query_id. | Server-Sent Events |
GET /queries | days, search, cache, feedback, min_latency_ms, before, limit | Project-scoped query history. | {items, next_cursor} |
GET /queries/<query-id> | - | Full recorded question, metrics, feedback note. | QueryRecord |
GET /health | - | Project-scoped readiness, activity, and feedback diagnostics. | {generated_at, query_window_days, projects: [ProjectHealth]} |
PUT /queries/<query-id>/feedback | {rating: "helpful" or "not_helpful", note?: string (max 1000)} | Replace project-scoped answer feedback. | 200 {query_id, rating, note, updated_at} |
DELETE /queries/<query-id>/feedback | - | Remove feedback for a query in the project. | 204 |
POST /retrieve | {query, top_k? (default 5)} | Retrieval only (no LLM): cosine top-k over chunks. 503 if embedding provider unavailable. | list[SourceChunk] |
POST /explore | {query (1–4000), hops (0–3, default 1)} | Agentic-RAG: seeds on nearest chunks + memories, walks related links. 503 if no embedding key. | BrainExploreResponse (query, seeds[], nodes[], edges[]) |
POST /files | multipart uploads | Upload + ingest. Requires can_upload key (else 403). Uses project-default chunking/embedding. | 201 list[FileOut] |
POST /memory | {content (1–8000), tags[], pinned, source (default "mcp")} | Save a memory (best-effort embed; warning if stored without embedding). | 201 MemoryOut |
POST /memory/search | {query, top_k? (default 5)} | Cosine search over memories with embeddings. 503 if no embedding key. | list[MemorySearchResult] (MemoryOut + similarity) |
GET /memory/recent | ?limit= (1–50, default 10) | Recent memories, pinned first. | list[MemoryOut] |
DELETE /memory/<memory_id> | - | Delete a memory owned by the project. | 204 |
GET /memory-graph | - | Full static brain graph: project → files → sections → chunks + memories, with related edges. | MemoryGraphResponse (project, nodes, edges) |

Upload-endpoint guards (POST /v1/.../files)
| Guard | Limit | Failure |
|---|---|---|
| Key permission | can_upload must be true | 403 read-only |
| Non-empty payload | - | 422 |
| Files per request | ≤ 20 | 413 |
| Total files per project | ≤ 1000 | 413 |
| Upload rate | ≤ 60 / 60s (per project) | 429 |
| Per-file type | must contain extractable text | 400 |
| Per-file size | ≤ 50 MB | 413 |
Request rate limits
Every public /v1 request is counted against two budgets at once: the API key's own budget AND a project-wide budget shared by ALL of that project's keys. Windows are fixed 60-second slots. Exceeding either budget returns 429 with a Retry-After header (seconds until the window resets) - wait that long, then retry.
| Endpoints | Per API key | Per project (all keys combined) |
|---|---|---|
Standard - /query, /query/stream, /queries, /queries/<query-id>, /health, /queries/<query-id>/feedback, /retrieve, /memory, /memory/search, /memory/recent, project info | 120 / min | 300 / min |
Heavy - /explore, /memory-graph | 10 / min | 20 / min |
Uploads - POST /files | - | 60 files / min (separate counter) |
Related guards introduced with the limiter:
/explorehopsis capped at 1 for API-key callers (each hop multiplies vector scans).- Memories cap at 2000 per project - creating past the cap returns
413. - A
429withRetry-After: 10can also mean the upstream AI provider rate-limited the project's key. Same backoff behavior, different fix: raise your provider quota (or add a project-level key) instead of sending fewer requests.
Example: query
curl -X POST https://oreag.onrender.com/v1/projects/<project-id>/query \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"question": "What is our SLA?", "top_k": 8}'{
"answer": "Our standard SLA is 99.9% uptime [1].",
"sources": [
{ "filename": "sla.pdf", "page_number": 2, "chunk_index": 5, "content": "...", "similarity": 0.82 }
],
"model": "openai/gpt-4o-mini",
"latency_ms": 734
}QueryResponse also carries depth ("short" | "long"), sub_queries (the focused sub-queries a long question was split into), conversation_id (echoed when you pass one, else null), and the clarification pair needs_clarification / clarification_questions. When needs_clarification is true, the answer field holds the human-facing clarification prompt and clarification_questions lists the follow-ups. Pass an optional conversation_id (≤ 128 chars) to continue a thread; see Querying for the full agentic loop and conversation memory.
See Queries, Health, and Answer feedback for complete monitoring and rating examples.
MCP tools
| Tool | Params (defaults) | Backend call |
|---|---|---|
save_memory | content, tags=None, pinned=False | POST /memory |
search_memory | query, limit=5 | POST /memory/search (top_k=limit) |
list_recent_memory | limit=10 | GET /memory/recent?limit= |
delete_memory | memory_id | DELETE /memory/<memory_id> |
search_docs | query, top_k=5 | POST /retrieve |
ask_docs | question | POST /query |
add_document | filename, content | POST /files (multipart; needs upload-enabled key, else 403) |
get_memory_graph | (none) | GET /memory-graph |
explore_brain | query, hops=1 | POST /explore |
Limits & quotas
Size & rate limits
| Limit | Value | Applies to |
|---|---|---|
| Max upload size | 50 MB / file | Owner and /v1 uploads |
Files per /v1 upload request | 20 | /v1 only |
| Files per project | 1000 | /v1 only |
| Upload rate | 60 / minute (sliding 60s, per project) | /v1 only |
Owner/dashboard uploads are exempt from the per-request, per-project, and rate quotas - those apply only to
/v1.
Schema-level validation
| Field | Constraint |
|---|---|
chunk_size | 100–8000 |
chunk_overlap | ≥ 0 and < chunk_size |
top_k | 1–20 |
question / query | ≤ 4000 chars |
memory content | ≤ 8000 chars |
hops (explore) | 0–3 |
| provider key length | 8–500 chars |
project name | ≤ 200 chars |
memory/recent limit | 1–50 (default 10) |
memory/search top_k | default 5 |
retrieve top_k | default 5 |
RAG behavior tuning (server defaults)
| Setting | Value | Effect |
|---|---|---|
rag_memory_blend_k | 4 | Memories searched and blended into RAG context per query |
rag_memory_min_similarity | 0.35 | Minimum similarity for a memory to be added as a pseudo-source |
explore_seeds_per_type | 6 | Seed chunks and seed memories each |
explore_fanout | 4 | Neighbours expanded per relation during a hop |
explore_max_nodes | 50 | Node budget for an explore subgraph |
Supported upload file types
.pdf .docx .pptx .xlsx .xls .csv .md .txt .html .htm .json .xml .rtf .epub .odt .ods .odp · images .bmp .gif .jpg .jpeg .png .tif .tiff · audio .mp3 .wav .m4a · .eml .zip
Those convert via MarkItDown; any other extension ingests as plain text. Images (.jpg .jpeg .png) are AI-captioned via the project's answer model (OpenAI or Gemini); audio is transcribed with the uploader's own STT-capable keys (OpenAI, Gemini, Groq, Mistral, Sarvam), falling back to a free speech endpoint for short clips. Opaque binary files are rejected with 400; documents that convert to no text fail ingestion.
Webhooks
Configure Project → API → Webhooks. A project supports five endpoints, each with selected events and an encrypted signing secret shown once on creation/rotation. Public HTTPS receivers on port 443 are required. DNS is revalidated and the connection is pinned to a public IP for every attempt; redirects, local/private addresses, credentials in URLs, and custom ports are rejected. Webhooks observe both public API and Playground activity.

| Event | Data |
|---|---|
file.indexed | file_id, status |
file.failed | file_id, status |
evaluation.completed | run_id, status |
evaluation.failed | run_id, status |
evaluation.regressed | run_id, quality_report |
budget.threshold_reached | alert_id, budget_id, period_start, threshold_percent, amount_usd, spent_usd |
Project budgets emit budget events. Account-wide budget spend stays in Usage and is never forwarded to a project's integration. Payloads contain {id, type, created_at, project_id, data}; they exclude questions, answers, file contents, and provider credentials. Events are saved atomically with the underlying database change, then delivered asynchronously. No outgoing HTTP happens during the original request transaction.
Verify Oreag-Signature (v1=<hex HMAC-SHA256>) against Oreag-Timestamp + "." + raw_body, using the signing secret. Timestamp is Unix seconds. SDK helpers enforce five-minute freshness and constant-time signature comparison. Validate raw bytes before JSON parsing. Save the event ID with a unique constraint, queue work transactionally, then return 2xx promptly. Deliveries can repeat and arrive out of order; duplicates should be successful no-ops. Use the resource APIs when you need current state.
Failed delivery attempts retry up to eight times with exponential backoff (30 seconds initially, capped at one hour, plus jitter). The connection timeout is eight seconds and leases allow recovery after a worker crash. Any non-2xx, including redirects, is a failure. Delivery history shows the latest 50 events and up to 32 attempts per event, including manual retries. A failed delivery can be retried from the UI with the same event ID. Pausing holds pending deliveries and stops subscribing to new events. Removing an endpoint removes its queue/history. Rotating uses the new secret for future attempts; an in-flight attempt may use the previous one.
Owner API routes:
| Method | Path |
|---|---|
| GET, POST | /api/projects/{project_id}/webhooks |
| PATCH, DELETE | /api/projects/{project_id}/webhooks/{endpoint_id} |
| POST | /api/projects/{project_id}/webhooks/{endpoint_id}/rotate-secret |
| GET | /api/projects/{project_id}/webhooks/{endpoint_id}/deliveries |
| POST | /api/projects/{project_id}/webhooks/{endpoint_id}/deliveries/{delivery_id}/retry |
Create with {url: "https://your-app.com/events", events: ["file.indexed", "evaluation.regressed"]}; PATCH accepts {enabled: false}. Endpoint URLs/event subscriptions are immutable; replace an endpoint to change them. Configuration requires the owning dashboard session; project API keys do not expose signing secrets or manage endpoints. Delivery history is backend-owned, with no direct client table grants.
Python and JavaScript SDKs
Sign up or log in to Oreag, create a project, and generate an API key from the project's API tab. Configure the SDK with your API key and project ID on your server. You do not need an npm or PyPI account to install either package.
JavaScript / TypeScript — npm
Package: @haroontrailblazer/oreag-sdk on npm
npm install @haroontrailblazer/oreag-sdk
Requires Node.js 18+ and includes TypeScript declarations. Published version: 0.2.0.
Python — PyPI
Package: oreag-sdk on PyPI
pip install oreag-sdk
Requires Python 3.10+. Published version: 0.2.0.
The Python source download, JavaScript source download, and both repository SDK folders remain available. Keep API keys on your server.
Connect to your project
import os
from oreag import Oreag
with Oreag(os.environ["OREAG_API_KEY"], os.environ["OREAG_PROJECT_ID"]) as client:
for event in client.stream_query("What is the policy?", top_k=5):
if event["type"] == "token":
print(event["text"], end="", flush=True)import { OreagClient } from "@haroontrailblazer/oreag-sdk";
const client = new OreagClient({ apiKey: process.env.OREAG_API_KEY, projectId: process.env.OREAG_PROJECT_ID });
const response = await client.query("What is the policy?", { top_k: 5 });
// After the user supplies a rating, use the returned query_id:
await client.feedback(response.query_id, "not_helpful", "Missing the new policy");Both clients provide upload, query, streaming, feedback/clear feedback, query detail, Health, test-set reads/additions, and background evaluation start/read/cancel methods. Upload requires an upload-enabled key. Use file.indexed to know when uploaded content is ready. JavaScript streams accept AbortSignal; breaking iteration closes the reader. Close partially consumed Python generators to release their connection.
Errors expose status, detail, and Retry-After (retryAfter in JS, retry_after in Python). No calls retry automatically, since an interrupted request may already have incurred cost. Both SDKs preserve existing question, answer, sources, conversation and cache fields. Runnable upload/stream examples and signature-verification examples are included in each download.

Safe retries
Set an optional Idempotency-Key on POST /v1/projects/{project_id}/query, POST /v1/projects/{project_id}/files, or POST /v1/projects/{project_id}/evaluations/runs. Use 1–128 letters, digits, dots, underscores, colons or hyphens. Reuse the same key and identical content for one logical request after losing its response. Keys are scoped by API key and operation.
Completed responses are encrypted and retained for 24 hours. A completed retry returns the original payload and status with Idempotency-Replayed: true without performing the work again. Different content returns 409. Pending or uncertain requests return 409 with Retry-After; they are not executed again during retention. Inspect the resource before creating a new key for uncertain work. After expiry the key may execute anew. Authentication, revoked-key and suspension checks still apply.
answer = client.query("What is the policy?", idempotency_key="order-123-policy")const answer = await client.query("What is the policy?", { idempotencyKey: "order-123-policy" });The options also work for SDK uploads and evaluation creation. Streaming rejects this header with 422; interrupted streams are never automatically retried. Requests without the header keep the original API behavior and answer/question fields.
Releases and recovery
SDK 0.2.0 adds safe-retry options. JavaScript version 0.2.0 is published as @haroontrailblazer/oreag-sdk on npm. Python version 0.2.0 is published as oreag-sdk on PyPI. Automated registry releases still require trusted-publisher setup. Both SDKs retain the Oreag proprietary viewing-only license; downloading or publishing does not grant application-use rights. Contact the owner for permission beyond those terms.
The repository includes a staging load harness, PostgreSQL concurrency tests, encrypted database/file restore checks, and a weekly reliability workflow. Fixture results validate the tooling; they do not prove production capacity or production restore success. The operations runbook describes staging commands, restore coverage, target isolation, and registry setup.