Contact

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:

FieldRequiredNotes
nameyesHuman-readable label
descriptionnoFree-form notes
embedding_modelyesMust match a configured model for this stack. See GET /v1/models
dimensionsnoMRL 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_model matches the request — returns it unchanged: 200 OK. No new corpus is created.
  • Exactly one corpus with this name exists but its embedding_model differs from the request409 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 name409 Conflict. if_not_exists cannot guess which one you mean; delete/rename the duplicates or omit if_not_exists to 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 OK with { "deleted": true, "corpus_id": "…" }.
  • At or above 10,000 vectors — background job. Response is 202 Accepted with { "job_id": "…", "status": "pending" }. Poll GET /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/ingest and POST /v1/ingest-prepared.
  • Document browsing — Documents.
  • Vector metrics — Metrics.
  • Batch-setsBatch-sets.
  • Async job stateJobs.