Kyro API
Self-hosted RAG API. Upload documents, embed them locally, and ask questions grounded in your own files — no data leaves your server.
http://localhost:3000/api/v2MIT — free to self-hostOpenAPI 3.0.0X-Api-KeyHow to use Kyro
Kyro gives you three ways to interact with your documents — pick whichever fits your workflow. All three talk to the same backend running on your server.
Dashboard
A web UI built into Kyro. Upload files, manage collections, create API keys, view usage analytics, and ask questions — all without writing any code. Best for non-technical teammates or quick one-off tasks.
Dashboard guide →TypeScript SDK
The official kyro-sdk package. Typed methods for every
API operation — upload, embed, search, ask. The easiest way to integrate
Kyro into a Node.js or browser application.
HTTP API
Raw REST endpoints — useful if you're building in a language other than TypeScript, scripting with curl, or need direct control over request shape and headers.
HTTP reference →Dashboard
Kyro ships with a built-in web dashboard available at the root of your
instance (e.g. http://localhost:3000). It's the fastest
way to get started — no API calls required.
Register your organisation
On first run, navigate to your instance URL and register. This creates your organisation and owner account. Subsequent users are invited from the Settings page — the registration page locks after the first org is created.
Create an API key
Go to API Keys in the sidebar and create a key with the scopes you need. The full key is shown once — copy it immediately. You'll need it to use the SDK or HTTP API.
read — list files, search, ask questions
write — upload, delete, embed files
admin — full access including key managementUpload and manage files
The Files page lets you drag-and-drop uploads, track extraction and embedding status per file, download originals, and add files to collections. Text extraction and embedding are triggered automatically — you can also re-trigger them manually from the file row.
Organise with Collections
Collections group files so you can scope search and /ask to a specific set of documents — a matter, a product, a project.
Create a collection, add files, and use its ID in the SDK or API to
limit context.
Ask questions
The Ask page is a chat interface wired to the /ask endpoint. Select a scope (whole org or a specific
collection), type a question, and get a streamed answer with source
citations — no code required. Useful for testing retrieval quality
before building your own UI.
Monitor usage
The Usage page shows total requests, data in/out, storage consumed, and a daily request chart. Switch between 7, 30, and 90-day windows to track trends.
TypeScript SDK
kyro-sdk is the official client library for Node.js and the
browser. It wraps every HTTP endpoint with typed methods, handles streaming
SSE responses, and throws structured errors you can catch and branch on.
kyro-sdknpm i @kyroinfra/kyroNode.js · Browser · Edgev2npm i @kyroinfra/kyroimport { KyroClient } from "kyro-sdk";
const kyro = new KyroClient({
baseURL: "http://localhost:3000/api/v2",
apiKey: "kyro_live_...",
});
// Upload a file
const file = await kyro.files.upload(blob, "report.pdf");
// Ask a question (non-streaming)
const { answer, sources } = await kyro.files.ask({
question: "What are the key findings?",
stream: false,
});Typical workflow
End-to-end: upload → tag → embed → ask.
// 1. Upload
const file = await kyro.files.upload(pdfBlob, "agreement.pdf");
// 2. Tag with metadata
await kyro.files.setMetadata(file.id, {
document_type: "contract",
matter_number: "M-2024-001",
});
// 3. Add to a collection
await kyro.collections.addFiles(collectionId, { fileIds: [file.id] });
// 4. Poll until embedded
let status = (await kyro.files.getText(file.id)).extractionStatus;
while (status !== "completed") {
await new Promise(r => setTimeout(r, 2000));
status = (await kyro.files.getText(file.id)).extractionStatus;
}
await kyro.files.embed(file.id);
// 5. Ask questions
const { answer } = await kyro.files.ask({
question: "What are the termination conditions?",
collectionId: collectionId,
stream: false,
});SDK · Files
All file operations live on kyro.files.
kyro.files.upload(blob, filename)Upload a file. Returns the file record including its ID and initial extractionStatus. Supported types: PDF, DOCX, TXT — others are stored but not extracted.
const file = await kyro.files.upload(blob, "contract.pdf");
// file.id, file.extractionStatuskyro.files.list(limit?, cursor?)Paginated file list. Pass pagination.nextCursor as the cursor argument to fetch the next page.
const { data, pagination } = await kyro.files.list(50);
if (pagination.hasMore) {
const next = await kyro.files.list(50, pagination.nextCursor);
}kyro.files.download(fileId)Download the original file as a Blob.
const blob = await kyro.files.download(fileId);kyro.files.delete(fileId)Soft-delete a file and remove it from disk.
await kyro.files.delete(fileId);kyro.files.getText(fileId)Get extraction status and extracted text. Poll this after upload to know when the file is ready to embed.
const { extractionStatus, extractedText } = await kyro.files.getText(fileId);
// extractionStatus: "pending" | "processing" | "completed" | "failed" | "skipped"kyro.files.triggerExtraction(fileId)Manually trigger text extraction. Extraction runs automatically on upload when Redis is available — use this to re-trigger failed or skipped files.
await kyro.files.triggerExtraction(fileId);kyro.files.embed(fileId)Chunk and embed a file via your local Ollama model. Requires extractionStatus to be 'completed' first.
const { chunksCreated } = await kyro.files.embed(fileId);kyro.files.setMetadata(fileId, meta)Upsert arbitrary key-value metadata on a file. Non-destructive — keys not in the payload are left untouched.
await kyro.files.setMetadata(fileId, {
matter_number: "M-2024-001",
document_type: "contract",
});kyro.files.getMetadata(fileId)Return all metadata keys and values for a file.
const meta = await kyro.files.getMetadata(fileId);kyro.files.deleteMetadataKey(fileId, key)Remove a single metadata key.
await kyro.files.deleteMetadataKey(fileId, "counterparty");kyro.files.search(query, limit?)Full-text BM25 keyword search. Returns file-level results with highlighted snippets.
const { data } = await kyro.files.search("indemnification clause");
// data[i].headline — snippet with <mark> highlightskyro.files.semanticSearch({ query, ... })Hybrid BM25 + vector search with RRF. Scope to a collection, specific files, or the whole org.
const { data } = await kyro.files.semanticSearch({
query: "force majeure clause",
collectionId: collectionId,
limit: 10,
});
// data[i]: { fileId, fileName, chunkIndex, content, rrfScore }SDK · Collections
All collection operations live on kyro.collections. A
collection is a named group of files — scope search and /ask to it by passing its ID.
kyro.collections.create({ name, description? })Create a new collection. Name must be unique within your org.
const col = await kyro.collections.create({
name: "Q4 Contracts",
description: "All contracts signed in Q4 2024",
});kyro.collections.list()List all collections. Each item includes fileCount, embeddedCount, pendingCount, and failedCount.
const collections = await kyro.collections.list();kyro.collections.get(collectionId)Get a collection with full status. col.queryReady is true when all files are embedded.
const col = await kyro.collections.get(collectionId);
// col.queryReady — true when ready to searchkyro.collections.update(collectionId, { name?, description? })Update collection name or description.
await kyro.collections.update(collectionId, { name: "Q4 Agreements" });kyro.collections.delete(collectionId)Delete a collection. Does not delete the files themselves.
await kyro.collections.delete(collectionId);kyro.collections.addFiles(collectionId, { fileIds })Add up to 100 files to a collection in one call.
await kyro.collections.addFiles(collectionId, {
fileIds: [fileId1, fileId2],
});kyro.collections.listFiles(collectionId)List files in a collection with per-file embedding status.
const { data } = await kyro.collections.listFiles(collectionId);
// data[i].embeddingStatuskyro.collections.removeFile(collectionId, fileId)Remove a file from a collection without deleting it.
await kyro.collections.removeFile(collectionId, fileId);SDK · Ask & Search
kyro.files.ask(options)Ask a natural language question and get an answer grounded in your
documents, with source citations. Set stream: false for a single resolved value, or use askStream for token-by-token
output.
const { answer, sources } = await kyro.files.ask({
question: "What is the total contract value?",
collectionId: collectionId,
stream: false,
});
// sources[i]: { fileId, fileName, chunkIndex, content, rrfScore }for await (const event of kyro.files.askStream({ question: "Summarise the risks." })) {
switch (event.type) {
case "sources": console.log(event.sources); break;
case "chunk": process.stdout.write(event.text); break;
case "done": console.log("\nDone"); break;
case "error": console.error(event.message); break;
}
}| Option | Type | Default | Description |
|---|---|---|---|
| question | string | — | The question to answer |
| collectionId | string | — | Scope to a collection. Mutually exclusive with fileIds |
| fileIds | string[] | — | Scope to specific files. Mutually exclusive with collectionId |
| filters | object | — | Metadata key-value filters. AND across keys, OR across values |
| topK | number | 8 | Number of chunks to retrieve (max 20) |
| minScore | number | 0 | Minimum RRF score threshold |
| model | string | server default | Ollama model override |
| stream | boolean | true | Whether to stream the response as SSE |
SDK · Error Handling
All SDK methods throw typed errors. Import the specific class you want to catch — each maps to a distinct HTTP status code.
import {
KyroApiError, KyroAuthError, KyroNotFoundError,
KyroRateLimitError, KyroQuotaError, KyroValidationError,
} from "kyro-sdk";
try {
await kyro.files.upload(blob, "file.pdf");
} catch (err) {
if (err instanceof KyroAuthError) { /* 401 — invalid or missing API key */ }
if (err instanceof KyroQuotaError) { /* 403 — storage quota exceeded */ }
if (err instanceof KyroNotFoundError) { /* 404 — resource not found */ }
if (err instanceof KyroRateLimitError) { /* 429 — check err.retryAfter */
setTimeout(retry, err.retryAfter * 1000); }
if (err instanceof KyroValidationError) { /* 400 — err.details has field errors */ }
if (err instanceof KyroApiError) { /* catch-all */ }
}| Class | HTTP status | When it's thrown |
|---|---|---|
| KyroAuthError | 401 | Missing or invalid API key |
| KyroQuotaError | 403 | Storage quota exceeded |
| KyroNotFoundError | 404 | File, collection, or resource not found |
| KyroValidationError | 400 | Bad input — err.details has per-field messages |
| KyroRateLimitError | 429 | Rate limit hit — err.retryAfter is seconds to wait |
| KyroApiError | any | Catch-all for any other API error — err.status and err.code available |
Self-hosting
Kyro runs entirely on your own infrastructure. Postgres stores files and metadata, Redis queues background jobs, and Ollama runs your embedding and chat models locally. Nothing leaves your network.
Start the stack
One command brings up Postgres, Redis, and the Kyro API server.
docker compose up -dPull an Ollama model
Kyro uses nomic-embed-text for embeddings and llama3.2 for answering questions. Set OLLAMA_URL, EMBEDDING_MODEL, and CHAT_MODEL in
your .env to use different models.
ollama pull nomic-embed-text
ollama pull llama3.2Create an account and API key
Register via POST /api/v2/auth/register, then
create an API key from the dashboard or via POST /api/v2/keys. Keys carry one of three scopes.
read — list files, search, ask questions
write — upload, delete, embed files
admin — full access including key managementAuthentication
File and search endpoints use API keys. Dashboard and key-management
endpoints use a short-lived JWT obtained from /auth/login.
X-Api-Key header — file operations API KeyX-Api-Key: kyro_live_your_key_hereAuthorization header — org & key management Bearer JWTAuthorization: Bearer <token from /auth/login>RAG Pipeline
Every document goes through three stages before it can be queried. All processing runs on your server — no external API calls are made at any stage.
Upload
POST /files saves the file to disk and, if Redis is
running, automatically queues text extraction in the background.
Supported types: PDF, DOCX, TXT.
Extract
The BullMQ worker pulls text from the file, splits it into
overlapping chunks (~1200 chars each), and stores them in
Postgres. Poll GET /files/:id/text for status.
Re-trigger manually with POST /files/:id/extract.
Embed
After extraction, the worker calls your local Ollama embedding
model (nomic-embed-text by default) and stores
768-dimensional vectors alongside each chunk in pgvector. Trigger manually with POST /files/:id/embed.
/ask — grounded answers
Send a question, get a cited answer. Kyro retrieves the most relevant chunks via hybrid search, builds a grounded prompt, and streams the response from your local LLM. The LLM only sees what is in your documents — it cannot fabricate facts from outside them.
BM25 + pgvector → RRFCross-encoder via OllamaSSE stream (or JSON)file IDs, collection, or entire orgconst res = await fetch('/api/v2/files/ask', {
method: 'POST',
headers: { 'X-Api-Key': process.env.KYRO_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
question: 'What are the payment terms?',
collectionId: 'c_xyz', // or fileIds: [...], or omit for org-wide
topK: 8,
stream: true,
}),
});
for await (const event of parseSSE(res)) {
if (event.type === 'sources') console.log(event.sources);
if (event.type === 'chunk') process.stdout.write(event.text);
if (event.type === 'done') break;
}| SSE event type | Payload |
|---|---|
sources | Array of retrieved chunks with file name, chunk index, content, and RRF score. Sent before the first answer token. |
chunk | A token fragment from the LLM. Accumulate these to build the full answer. |
done | Stream complete. |
error | LLM or retrieval error. Includes a message field. |
/semantic-search — chunk retrieval
Returns ranked chunks without generating an answer. Useful for
building your own UI, debugging retrieval quality, or feeding results
into your own pipeline. Uses the same two-phase hybrid retrieval as /ask: BM25 keyword matching fused with pgvector cosine similarity via
Reciprocal Rank Fusion, optionally followed by cross-encoder
reranking.
curl "http://localhost:3000/api/v2/files/semantic-search?q=payment+terms&collection_id=c_xyz&limit=5" \
-H "X-Api-Key: $API_KEY"Metadata Filters
Every file can carry arbitrary key/value metadata — useful for tagging
documents with vertical-specific fields like matter_number, document_type, product, or version. Set metadata with PUT /files/:id/metadata, then pass a filters object to /ask to scope retrieval.
curl -X PUT http://localhost:3000/api/v2/files/f_abc123/metadata \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{\"matter_number\": \"M-2024-001\", \"document_type\": \"contract\"}'{
"question": "What are the termination clauses?",
"filters": {
"matter_number": "M-2024-001",
"document_type": ["contract", "amendment"]
}
}Filters are AND across keys and OR across values. The example above matches files
where matter_number is M-2024-001 and document_type is either contract or amendment.
Rate Limiting
The default limit is 100 requests per minute per API key, enforced in Redis (falls back to in-process memory if Redis is unavailable). Both limits are configurable in the server source — adjust to match your infrastructure capacity.
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when limit resets |
Retry-After | Seconds to wait before retrying (on 429) |
Error Codes
All errors return a consistent JSON body. Check error.message for a human-readable description.
{
"error": "API key does not have write scope"
}| Code | Label | Description |
|---|---|---|
| 400 | Bad Request | Invalid input or missing required fields |
| 401 | Unauthorized | Missing or invalid API key |
| 403 | Forbidden | Insufficient scope or storage quota exceeded |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Operation already in progress |
| 413 | Payload Too Large | File exceeds the 100 MB size limit |
| 422 | Unprocessable | No embedded files found for the requested scope — embed files first |
| 429 | Too Many Requests | Rate limit exceeded — check Retry-After header |
| 500 | Internal Server Error | Unexpected server-side error |
HTTP API Reference
All endpoints documented below. Authenticate file and search
operations with your API key in the X-Api-Key header.
Base URL defaults to http://localhost:3000/api/v2 — replace
with whatever host you deployed Kyro on.
X-Api-Key: kyro_live_your_key_hereFiles
6 endpointsUpload, list, download, delete, and extract text from files stored on your server.
/files List filesQuery Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | — | Max results (default 100, max 100). |
cursor | string | — | Pagination cursor from previous response. |
Response Codes
curl -X GET \
"http://localhost:3000/api/v2/files" \
-H "X-Api-Key: $API_KEY"{
"data": [
{
"id": "f_abc123",
"name": "contract.pdf",
"mimeType": "application/pdf",
"sizeBytes": 204800,
"createdAt": "2025-01-15T12:00:00Z",
"extractionStatus": "completed"
}
],
"pagination": {
"limit": 100,
"hasMore": false,
"nextCursor": null
}
}/files Upload fileRequest Body multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
file | File (binary) | ✓ | The file to upload. PDF, DOCX, and TXT are supported for text extraction. |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/files" \
-H "X-Api-Key: $API_KEY" \
-F "file=@./your-file.pdf"{
"id": "f_abc123",
"name": "contract.pdf",
"mimeType": "application/pdf",
"sizeBytes": 204800,
"createdAt": "2025-01-15T12:00:00Z",
"extractionStatus": "pending"
}/files/{id} Download filePath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | File UUID. |
Response Codes
curl -X GET \
"http://localhost:3000/api/v2/files/{id}" \
-H "X-Api-Key: $API_KEY"Binary file stream./files/{id} Delete filePath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | File UUID. |
Response Codes
curl -X DELETE \
"http://localhost:3000/api/v2/files/{id}" \
-H "X-Api-Key: $API_KEY"{
"message": "File deleted",
"id": "f_abc123"
}/files/{id}/extract Trigger text extractionPath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | File UUID. |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/files/{id}/extract" \
-H "X-Api-Key: $API_KEY"{
"fileId": "f_abc123",
"extractionStatus": "pending",
"message": "Extraction queued. Poll GET /files/:id/text for status."
}/files/{id}/text Get extracted textPath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | File UUID. |
Response Codes
curl -X GET \
"http://localhost:3000/api/v2/files/{id}/text" \
-H "X-Api-Key: $API_KEY"{
"fileId": "f_abc123",
"extractionStatus": "completed",
"extractedText": "This agreement is entered into..."
}Embeddings
3 endpointsTrigger chunk embedding via your local Ollama model and query embedded files.
/files/{id}/embed Embed file chunksPath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | File UUID. |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/files/{id}/embed" \
-H "X-Api-Key: $API_KEY"{
"fileId": "f_abc123",
"embeddingStatus": "completed",
"chunksCreated": 14
}/files/ask Ask a questionRequest Body application/json
| Field | Type | Required | Description |
|---|---|---|---|
question | string | ✓ | The question to answer from your documents. |
fileIds | string (uuid)[] | — | Scope search to specific files. Omit to search the entire org. |
collectionId | string (uuid) | — | Scope search to a collection. Mutually exclusive with fileIds. |
filters | object | — | Metadata key/value filters to scope the search. AND across keys, OR across values. |
topK | integer | — | Chunks to retrieve (default 8, max 20). |
stream | boolean | — | Stream the answer as SSE events (default true). |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/files/ask" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "\"question_value\""
}'// SSE stream:
data: {"type":"sources","sources":[{"fileId":"...","fileName":"contract.pdf","content":"...","rrfScore":0.031}]}
data: {"type":"chunk","text":"According to [1], the payment terms..."}
data: {"type":"done"}/files/semantic-search Semantic searchQuery Parameters
| Name | Type | Required | Description |
|---|---|---|---|
q | string | ✓ | Search query. |
limit | integer | — | Max chunks returned (default 10, max 50). |
file_ids | string | — | Comma-separated file UUIDs to scope the search. |
collection_id | string | — | Collection UUID to scope the search. |
min_score | number | — | Minimum RRF score threshold (default 0.01). |
Response Codes
curl -X GET \
"http://localhost:3000/api/v2/files/semantic-search" \
-H "X-Api-Key: $API_KEY"{
"data": [
{
"fileId": "f_abc123",
"fileName": "contract.pdf",
"chunkIndex": 2,
"content": "Payment is due within 30 days...",
"rrfScore": 0.031
}
],
"query": "payment terms",
"limit": 10,
"minScore": 0.01
}Collections
3 endpointsGroup files into named collections and query them as a unit.
/collections List collectionsResponse Codes
curl -X GET \
"http://localhost:3000/api/v2/collections" \
-H "X-Api-Key: $API_KEY"[
{
"id": "c_xyz",
"name": "Q1 Contracts",
"slug": "q1-contracts",
"fileCount": 12,
"embeddedCount": 11,
"pendingCount": 1,
"failedCount": 0,
"createdAt": "2025-01-10T09:00:00Z"
}
]/collections Create collectionRequest Body application/json
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✓ | Collection name (unique within your org). |
description | string | — | Optional description. |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/collections" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "\"name_value\""
}'{
"id": "c_xyz",
"name": "Q1 Contracts",
"slug": "q1-contracts",
"description": null,
"createdAt": "2025-01-10T09:00:00Z"
}/collections/{id}/files Add files to collectionPath Parameters
| Name | Type | Required | Description |
|---|---|---|---|
id | uuid | ✓ | Collection UUID. |
Request Body application/json
| Field | Type | Required | Description |
|---|---|---|---|
fileIds | string (uuid)[] | ✓ | Up to 100 file UUIDs to add. |
Response Codes
curl -X POST \
"http://localhost:3000/api/v2/collections/{id}/files" \
-H "X-Api-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileIds": "\"fileIds_value\""
}'{
"added": 3,
"collectionId": "c_xyz"
}Health
1 endpointCheck whether the Kyro backend and its dependencies are reachable.
/health Health checkResponse Codes
curl -X GET \
"http://localhost:3000/api/v2/health"{
"status": "ok",
"database": "connected",
"redis": "connected",
"uptime": 86400
}