/ API Reference
v2.0.0

Kyro API

Self-hosted RAG API. Upload documents, embed them locally, and ask questions grounded in your own files — no data leaves your server.

Default URL http://localhost:3000/api/v2
License MIT — free to self-host
Spec format OpenAPI 3.0.0
Auth X-Api-Key

How 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.

01 no code

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 →
03 advanced

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.

01

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.

02

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.

scopes
read   — list files, search, ask questions
write  — upload, delete, embed files
admin  — full access including key management
03

Upload 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.

04

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.

05

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.

06

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.

Dashboard vs API key scope — The dashboard authenticates with a session cookie (JWT). The API key is a separate credential used by the SDK and HTTP API. You manage API keys from the dashboard but use them outside it.
typescript sdk

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.

Package kyro-sdk
Install npm i @kyroinfra/kyro
Environments Node.js · Browser · Edge
Default API version v2
shell
npm i @kyroinfra/kyro
typescript
import { 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.

typescript
// 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.

typescript
const file = await kyro.files.upload(blob, "contract.pdf");
// file.id, file.extractionStatus
kyro.files.list(limit?, cursor?)

Paginated file list. Pass pagination.nextCursor as the cursor argument to fetch the next page.

typescript
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.

typescript
const blob = await kyro.files.download(fileId);
kyro.files.delete(fileId)

Soft-delete a file and remove it from disk.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
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.

typescript
const meta = await kyro.files.getMetadata(fileId);
kyro.files.deleteMetadataKey(fileId, key)

Remove a single metadata key.

typescript
await kyro.files.deleteMetadataKey(fileId, "counterparty");
kyro.files.search(query, limit?)

Full-text BM25 keyword search. Returns file-level results with highlighted snippets.

typescript
const { data } = await kyro.files.search("indemnification clause");
// data[i].headline — snippet with <mark> highlights
kyro.files.semanticSearch({ query, ... })

Hybrid BM25 + vector search with RRF. Scope to a collection, specific files, or the whole org.

typescript
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.

typescript
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.

typescript
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.

typescript
const col = await kyro.collections.get(collectionId);
// col.queryReady — true when ready to search
kyro.collections.update(collectionId, { name?, description? })

Update collection name or description.

typescript
await kyro.collections.update(collectionId, { name: "Q4 Agreements" });
kyro.collections.delete(collectionId)

Delete a collection. Does not delete the files themselves.

typescript
await kyro.collections.delete(collectionId);
kyro.collections.addFiles(collectionId, { fileIds })

Add up to 100 files to a collection in one call.

typescript
await kyro.collections.addFiles(collectionId, {
  fileIds: [fileId1, fileId2],
});
kyro.collections.listFiles(collectionId)

List files in a collection with per-file embedding status.

typescript
const { data } = await kyro.collections.listFiles(collectionId);
// data[i].embeddingStatus
kyro.collections.removeFile(collectionId, fileId)

Remove a file from a collection without deleting it.

typescript
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.

non-streaming
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 }
streaming
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;
  }
}
OptionTypeDefaultDescription
questionstringThe question to answer
collectionIdstringScope to a collection. Mutually exclusive with fileIds
fileIdsstring[]Scope to specific files. Mutually exclusive with collectionId
filtersobjectMetadata key-value filters. AND across keys, OR across values
topKnumber8Number of chunks to retrieve (max 20)
minScorenumber0Minimum RRF score threshold
modelstringserver defaultOllama model override
streambooleantrueWhether 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.

typescript
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 */ }
}
ClassHTTP statusWhen it's thrown
KyroAuthError401Missing or invalid API key
KyroQuotaError403Storage quota exceeded
KyroNotFoundError404File, collection, or resource not found
KyroValidationError400Bad input — err.details has per-field messages
KyroRateLimitError429Rate limit hit — err.retryAfter is seconds to wait
KyroApiErroranyCatch-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.

01

Start the stack

One command brings up Postgres, Redis, and the Kyro API server.

shell
docker compose up -d
02

Pull 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.

shell
ollama pull nomic-embed-text
ollama pull llama3.2
03

Create 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.

scopes
read   — list files, search, ask questions
write  — upload, delete, embed files
admin  — full access including key management

Authentication

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 Key
http
X-Api-Key: kyro_live_your_key_here
Authorization header — org & key management Bearer JWT
http
Authorization: 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.

01

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.

02

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.

03

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.

Retrieval BM25 + pgvector → RRF
Reranking Cross-encoder via Ollama
Response SSE stream (or JSON)
Scope file IDs, collection, or entire org
typescript
const 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 typePayload
sourcesArray of retrieved chunks with file name, chunk index, content, and RRF score. Sent before the first answer token.
chunkA token fragment from the LLM. Accumulate these to build the full answer.
doneStream complete.
errorLLM or retrieval error. Includes a message field.

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.

set metadata
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\"}'
filter in /ask
{
  "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.

100
requests / minute (default)
HeaderDescription
X-RateLimit-LimitMax requests per window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when limit resets
Retry-AfterSeconds to wait before retrying (on 429)

Error Codes

All errors return a consistent JSON body. Check error.message for a human-readable description.

json
{
  "error": "API key does not have write scope"
}
CodeLabelDescription
400Bad RequestInvalid input or missing required fields
401UnauthorizedMissing or invalid API key
403ForbiddenInsufficient scope or storage quota exceeded
404Not FoundResource does not exist
409ConflictOperation already in progress
413Payload Too LargeFile exceeds the 100 MB size limit
422UnprocessableNo embedded files found for the requested scope — embed files first
429Too Many RequestsRate limit exceeded — check Retry-After header
500Internal Server ErrorUnexpected server-side error
reference

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.

http
X-Api-Key: kyro_live_your_key_here

Files

6 endpoints

Upload, list, download, delete, and extract text from files stored on your server.

GET /files List files
X-Api-Key scope: read

Query Parameters

NameTypeRequiredDescription
limitintegerMax results (default 100, max 100).
cursorstringPagination cursor from previous response.

Response Codes

200 Paginated file list.
401 Missing or invalid API key.
Request
curl -X GET \
  "http://localhost:3000/api/v2/files" \
  -H "X-Api-Key: $API_KEY"
Response 200
{
  "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
  }
}
POST /files Upload file
X-Api-Key scope: write

Request Body multipart/form-data

FieldTypeRequiredDescription
file File (binary) The file to upload. PDF, DOCX, and TXT are supported for text extraction.

Response Codes

201 File uploaded. Text extraction queued automatically if Redis is available.
401 Missing or invalid API key.
403 Storage quota exceeded.
413 File exceeds the 100 MB limit.
Request
curl -X POST \
  "http://localhost:3000/api/v2/files" \
  -H "X-Api-Key: $API_KEY" \
  -F "file=@./your-file.pdf"
Response 200
{
  "id": "f_abc123",
  "name": "contract.pdf",
  "mimeType": "application/pdf",
  "sizeBytes": 204800,
  "createdAt": "2025-01-15T12:00:00Z",
  "extractionStatus": "pending"
}
GET /files/{id} Download file
X-Api-Key scope: read

Path Parameters

NameTypeRequiredDescription
iduuidFile UUID.

Response Codes

200 Binary file stream.
401 Unauthorized.
404 File not found.
Request
curl -X GET \
  "http://localhost:3000/api/v2/files/{id}" \
  -H "X-Api-Key: $API_KEY"
Response 200
Binary file stream.
DELETE /files/{id} Delete file
X-Api-Key scope: write

Path Parameters

NameTypeRequiredDescription
iduuidFile UUID.

Response Codes

200 File soft-deleted and removed from disk.
401 Unauthorized.
404 File not found.
Request
curl -X DELETE \
  "http://localhost:3000/api/v2/files/{id}" \
  -H "X-Api-Key: $API_KEY"
Response 200
{
  "message": "File deleted",
  "id": "f_abc123"
}
POST /files/{id}/extract Trigger text extraction
X-Api-Key scope: write

Path Parameters

NameTypeRequiredDescription
iduuidFile UUID.

Response Codes

200 Extraction completed synchronously (no Redis).
202 Extraction queued (Redis available).
400 File type not supported for extraction.
409 Extraction already in progress.
Request
curl -X POST \
  "http://localhost:3000/api/v2/files/{id}/extract" \
  -H "X-Api-Key: $API_KEY"
Response 200
{
  "fileId": "f_abc123",
  "extractionStatus": "pending",
  "message": "Extraction queued. Poll GET /files/:id/text for status."
}
GET /files/{id}/text Get extracted text
X-Api-Key scope: read

Path Parameters

NameTypeRequiredDescription
iduuidFile UUID.

Response Codes

200 Extraction status and text if available.
404 File not found.
Request
curl -X GET \
  "http://localhost:3000/api/v2/files/{id}/text" \
  -H "X-Api-Key: $API_KEY"
Response 200
{
  "fileId": "f_abc123",
  "extractionStatus": "completed",
  "extractedText": "This agreement is entered into..."
}

Embeddings

3 endpoints

Trigger chunk embedding via your local Ollama model and query embedded files.

POST /files/{id}/embed Embed file chunks
X-Api-Key scope: write

Path Parameters

NameTypeRequiredDescription
iduuidFile UUID.

Response Codes

200 Chunks embedded via local Ollama model.
400 No extracted text available — run extraction first.
404 File not found.
Request
curl -X POST \
  "http://localhost:3000/api/v2/files/{id}/embed" \
  -H "X-Api-Key: $API_KEY"
Response 200
{
  "fileId": "f_abc123",
  "embeddingStatus": "completed",
  "chunksCreated": 14
}
POST /files/ask Ask a question
X-Api-Key scope: read

Request Body application/json

FieldTypeRequiredDescription
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

200 SSE stream of sources, answer chunks, and done event.
422 No embedded files found matching the scope.
Request
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\""
  }'
Response 200
// 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"}

Collections

3 endpoints

Group files into named collections and query them as a unit.

GET /collections List collections
X-Api-Key scope: read

Response Codes

200 Collections with embedding status summary.
Request
curl -X GET \
  "http://localhost:3000/api/v2/collections" \
  -H "X-Api-Key: $API_KEY"
Response 200
[
  {
    "id": "c_xyz",
    "name": "Q1 Contracts",
    "slug": "q1-contracts",
    "fileCount": 12,
    "embeddedCount": 11,
    "pendingCount": 1,
    "failedCount": 0,
    "createdAt": "2025-01-10T09:00:00Z"
  }
]
POST /collections Create collection
X-Api-Key scope: write

Request Body application/json

FieldTypeRequiredDescription
name string Collection name (unique within your org).
description string Optional description.

Response Codes

201 Collection created.
Request
curl -X POST \
  "http://localhost:3000/api/v2/collections" \
  -H "X-Api-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "\"name_value\""
  }'
Response 200
{
  "id": "c_xyz",
  "name": "Q1 Contracts",
  "slug": "q1-contracts",
  "description": null,
  "createdAt": "2025-01-10T09:00:00Z"
}
POST /collections/{id}/files Add files to collection
X-Api-Key scope: write

Path Parameters

NameTypeRequiredDescription
iduuidCollection UUID.

Request Body application/json

FieldTypeRequiredDescription
fileIds string (uuid)[] Up to 100 file UUIDs to add.

Response Codes

200 Files added.
400 Some file IDs not found or not owned by your org.
Request
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\""
  }'
Response 200
{
  "added": 3,
  "collectionId": "c_xyz"
}

Health

1 endpoint

Check whether the Kyro backend and its dependencies are reachable.

GET /health Health check
No auth

Response Codes

200 All dependencies healthy.
503 One or more dependencies degraded.
Request
curl -X GET \
  "http://localhost:3000/api/v2/health"
Response 200
{
  "status": "ok",
  "database": "connected",
  "redis": "connected",
  "uptime": 86400
}