REST API - knowledge
Collections and documents form the knowledge surface under /v1. Semantic search providers (SSP) back the vector store. The internal-collections subsystem enables semantic search over agents, graphs, and collections via a CDC-driven index.
Toolsets and tools: how agents access knowledge collections via the search toolset.
Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /v1/ssp |
Create a semantic-search provider |
| GET | /v1/ssp |
List providers |
| GET | /v1/ssp/{id} |
Fetch one provider |
| PUT | /v1/ssp/{id} |
Replace a provider |
| DELETE | /v1/ssp/{id} |
Delete (blocked if collections reference it) |
| POST | /v1/ssp/find |
Predicate-based search |
| POST | /v1/ssp/{id}/invalidate |
Invalidate cached connections |
| POST | /v1/embedding_providers |
Register an embedding provider |
| GET | /v1/embedding_providers |
List providers |
| GET | /v1/embedding_providers/{id} |
Fetch one provider |
| PUT | /v1/embedding_providers/{id} |
Replace a provider |
| DELETE | /v1/embedding_providers/{id} |
Delete |
| POST | /v1/collections |
Create a collection |
| GET | /v1/collections |
List collections |
| GET | /v1/collections/{id} |
Fetch one collection |
| PUT | /v1/collections/{id} |
Replace a collection |
| DELETE | /v1/collections/{id} |
Delete |
| POST | /v1/collections/find |
Predicate-based search |
| POST | /v1/collections/{id}/search |
Semantic search within a collection |
| GET | /v1/collections/{id}/documents |
List documents (no path) or read one by ?path= |
| PUT | /v1/collections/{id}/documents?path=<p> |
Create or replace a document at a path |
| DELETE | /v1/collections/{id}/documents?path=<p> |
Delete a document by path |
| POST | /v1/collections/{id}/documents/move |
Move a document from one path to another |
| GET | /v1/collections/{id}/indexed_documents |
List indexed chunks |
| POST | /v1/documents |
Create a document (entity CRUD; path required) |
| GET | /v1/documents/{id} |
Fetch one document by id |
| PUT | /v1/documents/{id} |
Replace and re-index by id |
| DELETE | /v1/documents/{id} |
Delete and remove chunks by id |
| POST | /v1/documents/_convert_file |
Convert a file to indexable text |
| PUT | /v1/internal_collections/config |
Activate the internal-collections subsystem |
| GET | /v1/internal_collections/config |
Get current config |
| DELETE | /v1/internal_collections/config |
Deactivate the subsystem |
| POST | /v1/internal_collections/bootstrap |
Trigger async bootstrap (returns 202) |
| GET | /v1/internal_collections/bootstrap/status |
Poll bootstrap status |
| POST | /v1/agents/search |
Semantic search over agents |
| POST | /v1/collections/search |
Semantic search over collections |
| POST | /v1/graphs/search |
Semantic search over graphs |
| POST | /v1/tools/search |
Semantic search over tools |
POST /v1/ssp
Create a semantic-search provider. Supported backends: pgvector, pgvectorscale, lance. The id is optional: supply one to use it verbatim, or omit it and the server assigns a type-prefixed id (e.g. semantic-search-provider-3f9a1c8d). The id is immutable after creation.
pgvector
curl -s -X POST https://primer.example/v1/ssp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "pg-main",
"provider": "pgvector",
"config": {
"hostname": "localhost",
"port": 5432,
"database": "primer_prod",
"username": "primer",
"password": "secret",
"db_schema": "public",
"distance_metric": "cosine"
}
}'import httpx
r = httpx.post(
"https://primer.example/v1/ssp",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "pg-main",
"provider": "pgvector",
"config": {
"hostname": "localhost",
"port": 5432,
"database": "primer_prod",
"username": "primer",
"password": "secret",
"db_schema": "public",
"distance_metric": "cosine",
},
},
)
r.raise_for_status()await fetch("/v1/ssp", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "pg-main",
provider: "pgvector",
config: {
hostname: "localhost",
port: 5432,
database: "primer_prod",
username: "primer",
password: "secret",
db_schema: "public",
distance_metric: "cosine",
},
}),
});Response 201:
{
"id": "pg-main",
"provider": "pgvector",
"config": { "hostname": "localhost", "port": 5432, "database": "primer_prod", "username": "primer", "db_schema": "public", "distance_metric": "cosine" }
}
Deleting an SSP that is still referenced by one or more collections returns 409 with /errors/conflict.
POST /v1/embedding_providers
Register an embedding provider. Supported backends: openai (compatible with LM Studio, Azure, OpenRouter, etc.), huggingface, and gemini. The id is optional: supply one to use it verbatim, or omit it and the server assigns a type-prefixed id (e.g. embedding-provider-3f9a1c8d). The id is immutable after creation.
curl -s -X POST https://primer.example/v1/embedding_providers \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "lmstudio-embed",
"provider": "openai",
"models": [{ "name": "nomic-embed-text-v1.5" }],
"config": {
"url": "http://localhost:1234/v1",
"api_key": "lm-studio",
"flavor": "lmstudio"
},
"limits": { "max_concurrency": 2 }
}'import httpx
r = httpx.post(
"https://primer.example/v1/embedding_providers",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "lmstudio-embed",
"provider": "openai",
"models": [{"name": "nomic-embed-text-v1.5"}],
"config": {
"url": "http://localhost:1234/v1",
"api_key": "lm-studio",
"flavor": "lmstudio",
},
"limits": {"max_concurrency": 2},
},
)
r.raise_for_status()await fetch("/v1/embedding_providers", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "lmstudio-embed",
provider: "openai",
models: [{ name: "nomic-embed-text-v1.5" }],
config: { url: "http://localhost:1234/v1", api_key: "lm-studio", flavor: "lmstudio" },
limits: { max_concurrency: 2 },
}),
});POST /v1/collections
Create a collection. Both embedder.provider_id and search_provider_id must reference existing rows. Passing an unknown search_provider_id returns 404. The id is optional: supply one to use it verbatim, or omit it and the server assigns a type-prefixed id (e.g. collection-3f9a1c8d). The id is immutable after creation.
curl -s -X POST https://primer.example/v1/collections \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "company-docs",
"description": "Internal company documentation",
"embedder": {
"provider_id": "lmstudio-embed",
"model": "nomic-embed-text-v1.5"
},
"search_provider_id": "pg-main",
"system": false
}'import httpx
r = httpx.post(
"https://primer.example/v1/collections",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "company-docs",
"description": "Internal company documentation",
"embedder": {
"provider_id": "lmstudio-embed",
"model": "nomic-embed-text-v1.5",
},
"search_provider_id": "pg-main",
"system": False,
},
)
r.raise_for_status()await fetch("/v1/collections", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "company-docs",
description: "Internal company documentation",
embedder: { provider_id: "lmstudio-embed", model: "nomic-embed-text-v1.5" },
search_provider_id: "pg-main",
system: false,
}),
});Response 201:
{
"id": "company-docs",
"description": "Internal company documentation",
"embedder": { "provider_id": "lmstudio-embed", "model": "nomic-embed-text-v1.5" },
"search_provider_id": "pg-main",
"system": false,
"harness_id": null,
"search": null
}
POST /v1/documents
Create a document via the id-addressed entity CRUD path. A document now carries a required path (a POSIX-like address, unique per collection, e.g. concepts/slo.md); creating one without path returns 422. The body to index is read from meta.text (or meta.content) on this CRUD path. Indexing is best-effort: when the embedder is reachable the document's chunks become searchable, and on embedder failure the row is persisted without chunks and indexing is retried later. The id is optional: supply one to use it verbatim, or omit it and the server assigns a type-prefixed id (e.g. document-3f9a1c8d). The id is immutable after creation. To address a document by its path instead, use the path-addressed routes under /v1/collections/{id}/documents (below).
curl -s -X POST https://primer.example/v1/documents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "doc-postmortem-001",
"collection_id": "company-docs",
"name": "Q1 post-mortem",
"path": "postmortems/q1.md",
"meta": { "text": "We experienced a 30-minute outage on March 3..." }
}'import httpx
r = httpx.post(
"https://primer.example/v1/documents",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "doc-postmortem-001",
"collection_id": "company-docs",
"name": "Q1 post-mortem",
"path": "postmortems/q1.md",
"meta": {"text": "We experienced a 30-minute outage on March 3..."},
},
)
r.raise_for_status()await fetch("/v1/documents", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "doc-postmortem-001",
collection_id: "company-docs",
name: "Q1 post-mortem",
path: "postmortems/q1.md",
meta: { text: "We experienced a 30-minute outage on March 3..." },
}),
});PUT /v1/documents/{id}
Replace a document by id and re-index it. The path field is required here too. Old chunks are removed before new chunks are written; no stale vectors linger.
curl -s -X PUT https://primer.example/v1/documents/doc-postmortem-001 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "doc-postmortem-001",
"collection_id": "company-docs",
"name": "Q1 post-mortem (revised)",
"path": "postmortems/q1.md",
"meta": { "text": "Updated analysis: root cause was a misconfigured timeout..." }
}'import httpx
r = httpx.put(
"https://primer.example/v1/documents/doc-postmortem-001",
headers={"Authorization": f"Bearer {token}"},
json={
"id": "doc-postmortem-001",
"collection_id": "company-docs",
"name": "Q1 post-mortem (revised)",
"path": "postmortems/q1.md",
"meta": {"text": "Updated analysis: root cause was a misconfigured timeout..."},
},
)
r.raise_for_status()await fetch("/v1/documents/doc-postmortem-001", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
id: "doc-postmortem-001",
collection_id: "company-docs",
name: "Q1 post-mortem (revised)",
path: "postmortems/q1.md",
meta: { text: "Updated analysis: root cause was a misconfigured timeout..." },
}),
});Path-addressed documents
Documents are also addressable by their path within a collection. These routes are the agent-facing surface: the body lives in a content store keyed by (collection_id, path), and the ?path= query form (rather than a slash-bearing path segment) avoids segment-routing issues with nested paths.
GET /v1/collections/{id}/documents
With no path, list documents in the collection. The listing is sourced from the content store unioned with entity rows that carry a path, is scoped to the collection, and is NOT offset-paginated. Bodies are not loaded; each entry carries document_id, path, and size (body length in characters). Pass ?prefix=<p> to scope the listing to a path prefix.
curl -s "https://primer.example/v1/collections/company-docs/documents" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/collections/company-docs/documents",
headers={"Authorization": f"Bearer {token}"},
)
docs = r.json()["documents"]const r = await fetch("/v1/collections/company-docs/documents", {
headers: { "Authorization": `Bearer ${token}` },
});
const { documents } = await r.json();Response 200:
{
"documents": [
{ "document_id": "doc-postmortem-001", "path": "postmortems/q1.md", "size": 48 }
]
}
With ?path=<p>, return the single document at that path (body + metadata), or 404:
curl -s "https://primer.example/v1/collections/company-docs/documents?path=postmortems/q1.md" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/collections/company-docs/documents",
params={"path": "postmortems/q1.md"},
headers={"Authorization": f"Bearer {token}"},
)
result = r.json()const r = await fetch(
"/v1/collections/company-docs/documents?path=postmortems/q1.md",
{ headers: { "Authorization": `Bearer ${token}` } },
);
const result = await r.json();Response 200:
{
"document": {
"id": "doc-postmortem-001",
"collection_id": "company-docs",
"name": "Q1 post-mortem",
"path": "postmortems/q1.md",
"title": null,
"meta": {}
},
"content": "We experienced a 30-minute outage on March 3..."
}
PUT /v1/collections/{id}/documents (path query)
Create or replace (upsert) the document at (collection_id, path) named by the ?path= query parameter. The body carries content (required), and optional title and meta. Returns the stored document metadata under document.
curl -s -X PUT "https://primer.example/v1/collections/company-docs/documents?path=postmortems/q1.md" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"content": "We experienced a 30-minute outage on March 3...", "title": "Q1 post-mortem"}'import httpx
r = httpx.put(
"https://primer.example/v1/collections/company-docs/documents",
params={"path": "postmortems/q1.md"},
headers={"Authorization": f"Bearer {token}"},
json={"content": "We experienced a 30-minute outage on March 3...", "title": "Q1 post-mortem"},
)
r.raise_for_status()await fetch("/v1/collections/company-docs/documents?path=postmortems/q1.md", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ content: "We experienced a 30-minute outage on March 3...", title: "Q1 post-mortem" }),
});DELETE /v1/collections/{id}/documents (path query)
Delete the document at (collection_id, path) named by the ?path= query parameter. Returns 204; a missing path returns 404.
curl -s -X DELETE "https://primer.example/v1/collections/company-docs/documents?path=postmortems/q1.md" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.delete(
"https://primer.example/v1/collections/company-docs/documents",
params={"path": "postmortems/q1.md"},
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status() # 204await fetch("/v1/collections/company-docs/documents?path=postmortems/q1.md", {
method: "DELETE",
headers: { "Authorization": `Bearer ${token}` },
});POST /v1/collections/{id}/documents/move
Move a document from one path to another within the collection. The body uses from and to. Returns 204; a missing source returns 404, and an already-occupied destination returns 409.
curl -s -X POST "https://primer.example/v1/collections/company-docs/documents/move" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"from": "postmortems/q1.md", "to": "archive/postmortems/q1.md"}'import httpx
r = httpx.post(
"https://primer.example/v1/collections/company-docs/documents/move",
headers={"Authorization": f"Bearer {token}"},
json={"from": "postmortems/q1.md", "to": "archive/postmortems/q1.md"},
)
r.raise_for_status() # 204await fetch("/v1/collections/company-docs/documents/move", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ from: "postmortems/q1.md", to: "archive/postmortems/q1.md" }),
});POST /v1/collections/{id}/search
Semantic search within a single collection. Returns a ranked list of matching chunks.
curl -s -X POST https://primer.example/v1/collections/company-docs/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "what caused the outage", "top_k": 5}'import httpx
r = httpx.post(
"https://primer.example/v1/collections/company-docs/search",
headers={"Authorization": f"Bearer {token}"},
json={"query": "what caused the outage", "top_k": 5},
)
hits = r.json()["hits"]const r = await fetch("/v1/collections/company-docs/search", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "what caused the outage", top_k: 5 }),
});
const { hits } = await r.json();Response 200:
{
"hits": [
{
"document_id": "doc-postmortem-001",
"chunk_id": "doc-postmortem-001:0",
"score": 0.91,
"text": "We experienced a 30-minute outage on March 3...",
"meta": {}
}
]
}
top_k is capped at 100. An empty collection returns {"hits": []} rather than an error.
GET /v1/collections/{id}/indexed_documents
List chunks that have been embedded into the vector store. Filter by ?document_id=<id> to inspect one document's chunks.
curl -s "https://primer.example/v1/collections/company-docs/indexed_documents?document_id=doc-postmortem-001" \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.get(
"https://primer.example/v1/collections/company-docs/indexed_documents",
params={"document_id": "doc-postmortem-001"},
headers={"Authorization": f"Bearer {token}"},
)
result = r.json()const r = await fetch(
"/v1/collections/company-docs/indexed_documents?document_id=doc-postmortem-001",
{ headers: { "Authorization": `Bearer ${token}` } },
);
const result = await r.json();Response 200:
{
"total": 1,
"items": [
{
"document_id": "doc-postmortem-001",
"chunk_id": "doc-postmortem-001:0",
"text": "We experienced a 30-minute outage on March 3..."
}
]
}
POST /v1/documents/_convert_file
Convert an uploaded file (markdown, PDF, etc.) to plain text suitable for ingestion. Uses multipart form data.
curl -s -X POST https://primer.example/v1/documents/_convert_file \
-H "Authorization: Bearer $TOKEN" \
-F "file=@note.md;type=text/markdown"import httpx
with open("note.md", "rb") as f:
r = httpx.post(
"https://primer.example/v1/documents/_convert_file",
headers={"Authorization": f"Bearer {token}"},
files={"file": ("note.md", f, "text/markdown")},
)
text = r.json()["text"]const fd = new FormData();
fd.append("file", fileBlob, "note.md");
const r = await fetch("/v1/documents/_convert_file", {
method: "POST",
headers: { "Authorization": `Bearer ${token}` },
body: fd,
});
const { text } = await r.json();Returns 200 with a JSON object carrying the extracted text plus upload metadata. The endpoint is non-destructive: it does not persist a Document row.
{
"filename": "note.md",
"content_type": "text/markdown",
"bytes_loaded": 1234,
"text": "# Note\n\n..."
}
The upload is capped at 32 MB; a larger or empty file returns 400.
Internal collections (SSP-backed semantic search for agents/graphs/collections/tools)
The internal-collections subsystem indexes agents, graphs, collections, and tools into a shared vector store so they can be found by semantic search. Activation requires a configured embedding provider and SSP. The config stores embedding_provider_id, embedding_model, and search_provider_id. Once activated, vector-space-defining fields are frozen; changing embedding_provider_id after the first bootstrap returns 409 with frozen_fields.
PUT /v1/internal_collections/config
Activate or update the subsystem config. Acts as an upsert before the first bootstrap. After bootstrap, changes to frozen fields return 409.
curl -s -X PUT https://primer.example/v1/internal_collections/config \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"embedding_provider_id": "lmstudio-embed",
"embedding_model": "nomic-embed-text-v1.5",
"search_provider_id": "pg-main"
}'import httpx
r = httpx.put(
"https://primer.example/v1/internal_collections/config",
headers={"Authorization": f"Bearer {token}"},
json={
"embedding_provider_id": "lmstudio-embed",
"embedding_model": "nomic-embed-text-v1.5",
"search_provider_id": "pg-main",
},
)
r.raise_for_status()await fetch("/v1/internal_collections/config", {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
embedding_provider_id: "lmstudio-embed",
embedding_model: "nomic-embed-text-v1.5",
search_provider_id: "pg-main",
}),
});POST /v1/internal_collections/bootstrap
Trigger an async bootstrap that creates vector tables and indexes existing entities. Returns 202 immediately with the freshly created status row (its status is "running"); poll GET /v1/internal_collections/bootstrap/status until status == "succeeded". A second call while the first is running returns 409.
curl -s -X POST https://primer.example/v1/internal_collections/bootstrap \
-H "Authorization: Bearer $TOKEN"import httpx, time
r = httpx.post(
"https://primer.example/v1/internal_collections/bootstrap",
headers={"Authorization": f"Bearer {token}"},
)
assert r.status_code == 202
# Poll until done
while True:
s = httpx.get(
"https://primer.example/v1/internal_collections/bootstrap/status",
headers={"Authorization": f"Bearer {token}"},
)
if s.json()["status"] == "succeeded":
break
time.sleep(1)const boot = await fetch("/v1/internal_collections/bootstrap", {
method: "POST",
headers: { "Authorization": `Bearer ${token}` },
});
// boot.status === 202; poll statusBootstrap 202 response body (the status row at launch):
{
"status": "running",
"phase": null,
"phase_done": 0,
"phase_total": null,
"counts": { "agents": 0, "graphs": 0, "collections": 0, "tools": 0 },
"started_at": "2026-06-08T12:00:00Z",
"finished_at": null,
"error": null
}
Status poll response 200 (same shape; status is one of idle, running, succeeded, failed):
{
"status": "succeeded",
"phase": null,
"phase_done": 0,
"phase_total": null,
"counts": { "agents": 4, "graphs": 2, "collections": 1, "tools": 5 },
"started_at": "2026-06-08T12:00:00Z",
"finished_at": "2026-06-08T12:00:12Z",
"error": null
}
DELETE /v1/internal_collections/config
Deactivate the subsystem. After deletion, search routes (/v1/agents/search, etc.) return 503 with /errors/subsystem-inactive.
curl -s -X DELETE https://primer.example/v1/internal_collections/config \
-H "Authorization: Bearer $TOKEN"import httpx
r = httpx.delete(
"https://primer.example/v1/internal_collections/config",
headers={"Authorization": f"Bearer {token}"},
)
r.raise_for_status() # 204await fetch("/v1/internal_collections/config", {
method: "DELETE",
headers: { "Authorization": `Bearer ${token}` },
});POST /v1/agents/search (and /collections/search, /graphs/search, /tools/search)
Semantic search over the internal collections. Requires a successful bootstrap. Returns the same SearchResponse shape for all four routes.
curl -s -X POST https://primer.example/v1/agents/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "data science automation", "top_k": 5}'import httpx
r = httpx.post(
"https://primer.example/v1/agents/search",
headers={"Authorization": f"Bearer {token}"},
json={"query": "data science automation", "top_k": 5},
)
hits = r.json()["hits"]const r = await fetch("/v1/agents/search", {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query: "data science automation", top_k: 5 }),
});
const { hits } = await r.json();Response 200:
{
"hits": [
{
"document_id": "agent-data-pipeline",
"chunk_id": "agent-data-pipeline:0",
"score": 0.88,
"text": "Automates data pipelines and runs ML experiments",
"meta": null
}
]
}
query must be non-empty (min length 1). top_k defaults to 10, max 100. If the subsystem is not bootstrapped, the route returns 503:
{
"type": "/errors/subsystem-inactive",
"title": "Subsystem Inactive",
"status": 503,
"detail": "internal collections subsystem is not active"
}
Error responses
All error responses follow RFC 7807:
{
"type": "/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "collection company-docs not found",
"instance": "/v1/collections/company-docs"
}
Common status codes: 404 resource not found, 409 conflict (SSP in use, frozen field, or duplicate), 422 validation error, 503 subsystem inactive.