Use the API
Everything the LocalRAG web page does goes through a small HTTP API, so you can use it from scripts and your own apps. The API has no authentication and listens on http://localhost:8090 by default. New to APIs? Start with What is an API? and HTTP basics.
Endpoints
| Method | Path | What it does |
|---|---|---|
POST | /api/ingest | Adds one or more documents. |
GET | /api/docs | Lists the documents. |
DELETE | /api/docs/{doc_id} | Deletes one document. |
POST | /api/query | Asks a question. The answer streams back as Server-Sent Events. |
GET | /api/health | Reports whether the vector store and Ollama are working. |
GET | / | The web page. |
Add documents
Send the files as a multipart form, with one files field per file. Allowed types are .pdf, .txt and .md.
curl -F "files=@manual.pdf" -F "files=@notes.md" http://localhost:8090/api/ingestThe response lists every file:
{
"documents": [
{"doc_id": "3f9a1c2b7d10", "filename": "manual.pdf", "pages": 14, "chunks": 38, "deduped": false},
{"doc_id": "8b02e4f1a9c3", "filename": "notes.md", "pages": 1, "chunks": 2, "deduped": false}
]
}doc_idis the first 12 characters of the file's SHA-256 hash. Use it to delete the document.dedupedistruewhen the same contents were already stored. Nothing new is added.
The request is all or nothing. If any file is unsupported or has no text, the server returns 400 with a message that names the file, and adds nothing:
{"detail": "scan.pdf: no extractable text in scan.pdf"}List documents
curl http://localhost:8090/api/docs{
"documents": [
{
"doc_id": "3f9a1c2b7d10",
"filename": "manual.pdf",
"sha256": "3f9a1c2b7d10…",
"pages": 14,
"chunks": 38,
"ingested_at": "2026-08-10T09:15:02.481193+00:00"
}
]
}Delete a document
curl -X DELETE http://localhost:8090/api/docs/3f9a1c2b7d10The server answers 204 No Content when the document is deleted, and 404 with unknown document id if there is no such document.
Ask a question
Send JSON with a question. top_k, the number of passages to retrieve, is optional; it defaults to 5 and is kept between 1 and 50.
curl -N -X POST http://localhost:8090/api/query \
-H "Content-Type: application/json" \
-d '{"question": "What is the warranty period?", "top_k": 5}'The -N option tells curl to print the answer as it arrives. A blank question returns 400 with question must not be blank.
The event stream
The response has the type text/event-stream (Server-Sent Events). It is a series of events, each with an event: line and a data: line of JSON:
event: token
data: {"text": "The warranty "}
event: token
data: {"text": "lasts two years."}
event: citations
data: [{"doc": "manual.pdf", "page": 12, "snippet": "Warranty. All pumps are covered for two years...", "score": 0.812}]
event: done
data: {}| Event | Data | When |
|---|---|---|
token | {"text": "..."} | Zero or more times: one piece of the answer each. Join them in order. |
citations | A list of {"doc", "page", "snippet", "score"} | Once, after the answer. One entry per page, best score first. snippet is the first 200 characters of the passage. |
error | {"message": "..."} | Instead of citations, if Ollama fails partway. Tokens already sent stay valid. |
done | {} | Always last. |
When nothing in your documents clears the score threshold, the stream is one token event with the text I don't know based on the indexed documents., an empty citations list, and done. Ollama is not called.
Read the stream in JavaScript
const res = await fetch('http://localhost:8090/api/query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ question: 'What is the warranty period?' })
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let answer = ''
while (true) {
const { value, done } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
let end
while ((end = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, end)
buffer = buffer.slice(end + 2)
const event = frame.match(/^event: (.*)$/m)[1]
const data = JSON.parse(frame.match(/^data: (.*)$/m)[1])
if (event === 'token') answer += data.text
if (event === 'citations') console.log('Sources:', data)
if (event === 'error') console.error(data.message)
}
}
console.log(answer)The browser's EventSource cannot send a POST body, so read the stream with fetch as above.
Check health
curl http://localhost:8090/api/health{"status": "ok", "chroma": true, "ollama": true, "model": "llama3.2", "docs": 3}| Field | Meaning |
|---|---|
status | ok when both parts work, otherwise degraded. |
chroma | true if the vector store answers. |
ollama | true if Ollama answers at the configured address. |
model | The Ollama model used for answers. |
docs | The number of stored documents. |
Security
The API has no login. With the default pip setup it only listens on 127.0.0.1, your own computer. The Docker setup publishes it on all network connections. Anyone who can reach the port can read from and delete your documents, so do not expose it to networks you do not trust.