Corpora
A corpus stores documents and their embedding vectors. Each corpus is bound to a single embedding model at creation time and lives inside a single environment.
See Concepts for the broader data model. This page documents every endpoint under /v1/corpora/*.
Create a corpus
POST /v1/corpora
{
"name": "product-docs",
"description": "Customer-facing product documentation",
"embedding_model": "text-embedding-3-small",
"dimensions": 1024
}
Fields:
| Field | Required | Notes |
|---|---|---|
name | yes | Human-readable label |
description | no | Free-form notes |
embedding_model | yes | Must match a configured model for this stack. See GET /v1/models |
dimensions | no | MRL truncation for capable models (e.g. text-embedding-3-large). Omit to use the model’s default dimensions |
Non-MRL models reject explicit dimensions that differ from the model’s fixed output — this guards against vector-name mismatches that would silently break ingest.
Response: 201 Created with the full CorpusDetail.
Get-or-create (if_not_exists)
POST /v1/corpora?if_not_exists=true
Names are not uniquely constrained per environment today, so plain POST /v1/corpora always creates a new corpus, even if one with the same name already exists. Callers that want idempotent “create the corpus for this name if it isn’t there yet” behavior — instead of hand-rolling GET /v1/corpora + client-side name scan + conditional create — should pass if_not_exists=true:
- No corpus with this name exists — behaves exactly like a normal create:
201 Created. - Exactly one corpus with this name exists and its
embedding_modelmatches the request — returns it unchanged:200 OK. No new corpus is created. - Exactly one corpus with this name exists but its
embedding_modeldiffers from the request —409 Conflict. The endpoint will never silently hand back a corpus bound to a different embedding model than the one you asked for. - More than one corpus already shares this name —
409 Conflict.if_not_existscannot guess which one you mean; delete/rename the duplicates or omitif_not_existsto create a new one explicitly.
This narrows, but does not fully close, the create race: the name lookup and the create are still two separate steps server-side. Two concurrent if_not_exists requests for a name that doesn’t exist yet can, in principle, both observe “not found” and both create. Genuinely atomic get-or-create would require a uniqueness guarantee at the corpus store, which does not exist today.
List corpora
GET /v1/corpora
Returns every corpus in the calling key’s environment, each decorated with pending_count (staged-but-uncommitted changes) and dirty (true when pending_count > 0).
[
{
"id": "787cfadf-ef36-46ec-8c23-8341fc2358cb",
"name": "product-docs",
"description": "Customer-facing product documentation",
"document_count": 142,
"embedding_count": 1083,
"dimensions": 1024,
"model": "text-embedding-3-small",
"pending_count": 3,
"dirty": true,
"created_at": "2026-04-10T12:34:56Z",
"default_voice_id": null
}
]
Get a corpus
GET /v1/corpora/{id}
Returns the corpus plus enrichment: model_metadata from the model registry (null on miss), last_ingest_at (ISO-8601 timestamp of the most recent completed ingest), and a search_latency placeholder that directs the portal UI to query POST /v1/logs/metrics for live p50/p95 numbers.
Update a corpus
PATCH /v1/corpora/{id}
{ "name": "renamed-docs" }
At least one of name or description must be present. The embedding model and dimensions are immutable — to change them, create a new corpus and re-ingest.
Delete a corpus
DELETE /v1/corpora/{id}
Routing is automatic based on how much vector data the corpus holds:
- Under 10,000 vectors — synchronous delete. Response is
200 OKwith{ "deleted": true, "corpus_id": "…" }. - At or above 10,000 vectors — background job. Response is
202 Acceptedwith{ "job_id": "…", "status": "pending" }. PollGET /v1/jobs/{id}for completion.
Deletion is irreversible — all documents, chunks, and embeddings are removed.
Inspect a corpus
Stats
GET /v1/corpora/{id}/stats
Lightweight counters: document_count, embedding_count, and size figures. Cheap to poll; suitable for health dashboards.
Vector-space metrics
GET /v1/corpora/{id}/metrics
Rich vector-space statistics (cosine-similarity histogram, vector-norm distribution, metadata-key population, dimension-activation distribution) with a 60-second server-side cache. Documented in full at Metrics.
Documents
GET /v1/corpora/{id}/documents
GET /v1/corpora/{id}/documents/{doc_id}/chunks
Browse the documents in a corpus or drill into a single document’s chunks. See Documents.
Batch-sets
GET /v1/corpora/{id}/batch-sets
List batch-sets (grouped provider batch jobs) for this corpus. Part of the J-024 batch-set lifecycle — see Batch-sets.
Staged changes
Ingest into Enscrive is a two-phase flow: stage changes into the corpus, then commit to embed and write to the live vector store. The staging endpoints under /corpora/{id}/… manage that middle state.
POST /v1/corpora/{id}/stage # add/update/delete entries in the staging area
GET /v1/corpora/{id}/pending # list everything staged but not yet committed
GET /v1/corpora/{id}/pending-status # summary: counts by op, any commit-in-progress
DELETE /v1/corpora/{id}/pending/{doc_id} # drop one staged entry
POST /v1/corpora/{id}/commit # launch a background job that embeds+promotes
POST /v1/corpora/{id}/revert # discard all pending changes
Staging is resumable: you can stage incrementally over many requests, then commit once. A commit that fails leaves the live corpus untouched — see Ingest for the full atomic-promote story.
Revert
POST /v1/corpora/{id}/revert
Drops every pending_changes row that is not currently part of an in-flight commit attempt. Useful for throwing away a misconfigured staging batch without affecting live data.
Response:
{
"corpus_id": "…",
"reverted_count": 12
}
Commit history
GET /v1/corpora/{id}/commits?limit=50&offset=0
Returns up to limit (max 200) of the most recent import_jobs whose params reference this corpus, in descending creation order. Each entry is a CommitSummary:
{
"id": "…",
"status": "complete",
"created_at": "2026-04-10T12:34:56Z",
"completed_at": "2026-04-10T12:37:12Z",
"documents_ingested": 42,
"documents_failed": 0
}
Use this to audit what was ingested when, and to correlate corpus drift with specific commits.
What this endpoint group does not cover
- Ingest itself — see Ingest for
POST /v1/ingestandPOST /v1/ingest-prepared. - Document browsing — Documents.
- Vector metrics — Metrics.
- Batch-sets — Batch-sets.
- Async job state — Jobs.