Document-Store API
Document-Store API
What it is. The document-store owns the lifecycle of documents — records that conform to a template and live in a namespace. It computes and enforces the template's identity hash on write (natural-upsert: writing the same identity again versions the document in place rather than erroring or duplicating), validates document data against the template's fields, and tracks version history per document. Beyond plain CRUD it also owns: file attachments (first-class entities with their own orphan/active/inactive lifecycle), relationship documents and BFS traversal between them (with an optional peer projection to avoid an N+1 fetch), a per-template table view for flattened/tabular consumption (JSON or CSV), template-version migration (re-pinning a cohort of documents from one template version to another without changing their identity), CSV/XLSX bulk import, replay sessions (republishing stored documents as events), and namespace backup job/restore job export/import.
Reaching it. Base path /api/document-store, mounted by the service's own APIRouter(prefix="/api/document-store") (components/document-store/src/document_store/api/__init__.py). See also: Routing & Ingress. Every route requires an API key — X-API-Key header, the ApiKeyAuth security scheme declared in schemas/document-store.json — enforced via require_api_key; most routes additionally call check_namespace_permission(identity, namespace, "read"|"write"|"admin") scoped to the namespace(s) the request resolves to. See also: Auth & Access.
Conventions that apply.
- Bulk-first 200 OK —
POST/PATCH/DELETE /documents,POST /documents/archive,POST /documents/migrate, andPATCH/DELETE /filesalways return HTTP 200 with aresultsarray of per-item outcomes (DocumentBulkResponse/DocumentBulkResultItem); a 200 does not mean every item succeeded — checkfailed/succeededand each item'sstatus/error_code. - Identity-hash natural-upsert —
POST /documentscreates a new document, or a new version of an existing one, purely from the template's declared identity fields; it never errors on a repeat write of the same identity. - PATCH semantics —
PATCH /documentsapplies an RFC 7396 JSON Merge Patch todataonly; identity fields andnamespacecannot be changed this way (usePOSTto create a new document instead). - Namespace scoping — every endpoint that touches a document, file, replay session, or backup job requires namespace permission at the appropriate level (read/write/admin) for every namespace the request resolves to.
See also: API Conventions; PoNIFs.
Endpoints
Examples below use $API for the mounted base URL (.../api/document-store) and $KEY for a valid X-API-Key value. Grouped by router (Documents, Validation, Table View, Files, Import, Replay, Backup, Health) in registration order (components/document-store/src/document_store/api/__init__.py); endpoints within each group are in source-file order.
Documents
POST /documents
- Purpose. Create or update one or more documents (bulk-first).
- Request. Body: array of
DocumentCreateRequest— requiredtemplate_id,namespace,data; optionaltemplate_version,document_id/version(pre-assigned, for restore/migration),created_by,metadata,synonyms,on_synonym_conflict("fail"default |"warn"). Query:continue_on_error(defaulttrue).template_idaccepts a canonical UUID or a registered synonym value, resolved before validation. - Response.
DocumentBulkResponse:results[](DocumentBulkResultItem:index,status—created/updated/skipped/error,id,document_id,identity_hash,version,is_new,warnings[]),total,succeeded,failed, optionaltiming. Single-item requests use a direct create path; multi-item requests use a batch path with cache warmup. - Errors. Per-item
error_codeon failure, includingsynonym_conflictandregistry_error(surfaced only for a single-item request, prefixed"<code>: <message>"by the service). Namespace write permission is required per distinct namespace in the batch. - Example.
curl -X POST "$API/documents" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '[{"template_id":"<template_id>","namespace":"<namespace>","data":{"...":"..."}}]' # {"results":[{"index":0,"status":"created","document_id":"<uuid7>","identity_hash":"<sha256>","version":1,"is_new":true,"warnings":[]}],"total":1,"succeeded":1,"failed":0}
PATCH /documents
- Purpose. Apply an RFC 7396 JSON Merge Patch to one or more existing documents, creating a new version each.
- Request. Body: array of
PatchDocumentItem— requireddocument_id,patch(objects deep-merge, arrays are replaced wholesale,nulldeletes a field), optionalif_match(expected current version; mismatch fails the item withconcurrency_conflict). Query:namespace(for synonym resolution only). - Response.
DocumentBulkResponse(same shape asPOST /documents). After merge, the full document is re-validated against the same template version already recorded on it, and a new version is written. - Errors. Identity fields and
namespacecannot be changed via patch. Archived documents are rejected — unarchive first. Write permission is checked per document's actual current namespace before mutation. - Example.
curl -X PATCH "$API/documents" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '[{"document_id":"<document_id>","patch":{"status":"closed"}}]'
POST /documents/migrate
- Purpose. Re-pin every active document currently on
from_versionof a template toto_version— the "move" half of the template-version lifecycle. No data transformation happens. - Request. Body
DocumentMigrateRequest: requiredtemplate_id,from_version,to_version; optionaldry_run(defaulttrue). Query:namespace(required unless the caller's API key is scoped to exactly one namespace). - Response.
DocumentMigrateResponse(extendsDocumentBulkResponsewithdry_run,template_id,from_version,to_version). Per-item statusupdated(applied or, underdry_run, projected) orerror. - Errors. Identity-preserving only: the two template versions must declare the same
identity_fields, else the whole operation is rejected (HTTP 409,identity_fields_changed). Also 400invalid_migration, 404template_not_found, 409target_inactive. Adry_run=truerun withfailed==0guarantees a successful apply barring concurrent writes. Admin permission required on the namespace. - Example.
curl -X POST "$API/documents/migrate?namespace=<namespace>" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"template_id":"<template_id>","from_version":1,"to_version":2,"dry_run":true}'
GET /documents
- Purpose. List documents with filtering and pagination.
- Request. Query:
namespace(omit for all accessible),template_id,template_value,status,latest_only(defaultfalse),page/page_size(default 1 / 50, max 1000),cursor(MongoDB_idof last item),sort_by(created_atdefault,updated_at,version, ordata.<path>),sort_order.template_idresolves via synonym lookup withstrict=True(a non-UUID value with no namespace context fails loud rather than silently matching zero rows). Whencursoris provided,pageis ignored,totalis-1, andsort_by/sort_ordermust be omitted (cursor mode is_id-ordered). - Response.
DocumentListResponse:items[DocumentResponse],total,page,page_size,pages,next_cursor. - Errors. 422 on an unresolvable
template_idor an invalid filter combination. - Example.
curl "$API/documents?namespace=<namespace>&latest_only=true" -H "X-API-Key: $KEY"
GET /documents/{document_id}
- Purpose. Get a document by its stable ID. Returns the latest version by default.
- Request. Path:
document_id(canonical UUID or registered synonym). Query:version(default: latest),namespace(for synonym resolution). - Response.
DocumentResponse. - Errors. 404 if not found. Read permission required on the document's namespace.
- Example.
curl "$API/documents/<document_id>" -H "X-API-Key: $KEY"
GET /documents/{document_id}/versions
- Purpose. Get all versions of a document.
- Request. Path:
document_id. Query:namespace. - Response.
DocumentVersionResponse:identity_hash,current_version,versions[DocumentVersionSummary](document_id,version,status,created_at,created_by). - Errors. 404 "Document not found".
- Example.
curl "$API/documents/<document_id>/versions" -H "X-API-Key: $KEY"
GET /documents/{document_id}/versions/{version}
- Purpose. Get one specific version of a document.
- Request. Path:
document_id,version. Query:namespace. - Response.
DocumentResponse. - Errors. 404 "Document version not found".
- Example.
curl "$API/documents/<document_id>/versions/2" -H "X-API-Key: $KEY"
GET /documents/{document_id}/latest
- Purpose. Get the latest version of a document given any of its version IDs — useful when holding a reference to an old version.
- Request. Path:
document_id. Query:namespace. - Response.
DocumentResponse(includes the latest document ID and version). - Errors. 404 "Document not found".
- Example.
curl "$API/documents/<document_id>/latest" -H "X-API-Key: $KEY"
GET /documents/{document_id}/relationships
- Purpose. List relationship documents (templates with
usage='relationship') touching this document. - Request. Path:
document_id. Query:direction(incoming|outgoing|both, defaultboth),template(comma-separated relationship template values),namespace(default: the document's own),active_only(defaulttrue),page/page_size(default 1 / 50, max 500),include(comma-separated; supportspeers— embeds a compact peer projection of the other-end entity per item). - Response.
RelationshipListResponse:items[RelationshipItem](aDocumentResponsepluspeer,peer_error_code—not_found|forbidden,peer_error),total,page,page_size,pages. Backed by Mongo indexes on(template_id, data.source_ref)/(template_id, data.target_ref)created lazily on the first relationship-document write. Default shape (no?include=peers) is unchanged from before peer projection existed. - Errors. 404 if the seed document doesn't exist; 400 on an invalid
direction/filter. - Example.
curl "$API/documents/<document_id>/relationships?include=peers" -H "X-API-Key: $KEY"
GET /documents/{document_id}/traverse
- Purpose. N-hop BFS traversal through relationship documents from a seed document.
- Request. Path:
document_id. Query:depth(1–10, default 1),types(comma-separated relationship template values),direction(outgoing|incoming|both, defaultoutgoing),namespace(default: seed's own). - Response.
TraverseResponse:seed_document_id,direction,depth,types_filter[],nodes[TraverseNode](document_id,template_id,namespace,depth,via_relationship,path[]),total_nodes,truncated. - Errors. 404 if the seed document doesn't exist; 400 on an invalid filter. Capped at
depth<=10andmax_nodes=1000;truncated=truewhen a cap fires. Visited documents are skipped, so cycles terminate. - Example.
curl "$API/documents/<document_id>/traverse?depth=3&direction=both" -H "X-API-Key: $KEY"
DELETE /documents
- Purpose. Delete one or more documents. Soft-delete by default.
- Request. Body: array of
DeleteItem—id, optionalversion(specific version to hard-delete; ignored for soft-delete),force(not valid for documents — see gotchas),hard_delete(defaultfalse; permanently removes, requires namespacedeletion_mode='full'),updated_by. Query:namespace. - Response.
DocumentBulkResponse; per-item statusdeletedorerror. - Errors. Per-item
error_code=force_unsupportedifforceis set (documents have no reference gate to override). Per-item error if the document doesn't exist. Write permission required on the document's namespace. - Example.
curl -X DELETE "$API/documents" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '[{"id":"<document_id>"}]'
POST /documents/archive
- Purpose. Archive one or more documents (sets status to
archived). - Request. Body: array of
ArchiveItem(id, optionalarchived_by). Query:namespace. - Response.
DocumentBulkResponse; per-item statusupdatedorerror. - Errors. Per-item error if the document doesn't exist. Write permission required.
- Example.
curl -X POST "$API/documents/archive" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '[{"id":"<document_id>"}]'
POST /documents/query
- Purpose. Query documents with complex filters (AND logic), including nested
datafields. - Request. Body
DocumentQueryRequest:filters[QueryFilter](field,operator—eq/ne/gt/gte/lt/lte/in/nin/exists/regex, defaulteq;value),template_id,status(defaultactive; passnullfor all versions),page/page_size(default 1 / 20, max 100),sort_by(defaultcreated_at),sort_order(defaultdesc). Query param:namespace. - Response.
DocumentQueryResponse:items[DocumentResponse],total,page,page_size,pages,query(echoes the resolved request). - Errors. 422 on an unresolvable
template_idwith no namespace context, or an invalid filter. - Example.
curl -X POST "$API/documents/query" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"filters":[{"field":"data.status","operator":"eq","value":"active"}]}'
GET /documents/by-identity/{identity_hash}
- Purpose. Get the active document with a specific identity hash.
- Request. Path:
identity_hash. Query:namespace,template_id(both recommended to avoid cross-template ambiguity),include_inactive(defaultfalse). - Response.
DocumentResponse. - Errors. 404 if not found.
- Example.
curl "$API/documents/by-identity/<identity_hash>?namespace=<namespace>" -H "X-API-Key: $KEY"
Validation
POST /validation/validate
- Purpose. Validate document data against a template without saving — pre-validation, or computing an identity hash.
- Request. Body
ValidationRequest:template_id,namespace(required, min length 1),data. - Response.
ValidationResponse:valid,errors[ValidationError](field,code,message,details),warnings[],identity_hash(if valid),template_version,term_references[],references[],file_references[]. - Errors. Read permission is checked unconditionally on
request.namespace(not gated behind a truthiness check, sincenamespaceis a required field) — validation reveals template structure and resolves term references against the namespace's term corpus. - Example.
curl -X POST "$API/validation/validate" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"template_id":"<template_id>","namespace":"<namespace>","data":{"...":"..."}}'
POST /validation/validate-bulk
- Purpose. Validate a batch of documents against ONE template, without saving — the bulk, side-effect-free counterpart to
/validate. - Request. Body
BulkValidationRequest:template_id,namespace(required),template_version(optional),items[](rawdatapayloads, one per document). - Response.
BulkValidationResponse:results[ValidationResponse], one per input item in the same order. The template (and its nested term/template references) is warmed into cache once, then each item validates from cache. Zero persistence — no documents, versions, or identity-hash registry side effects; identity hashes are computed locally. - Errors. An unresolvable
template_idfails the whole request with 404 (the batch is single-template). An individual invalid document is reported as that item'svalid: false+errors, not a batch error. - Example.
curl -X POST "$API/validation/validate-bulk" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"template_id":"<template_id>","namespace":"<namespace>","items":[{"...":"..."}]}'
Table View
GET /table/{template_id}
- Purpose. Return documents for a template as a flattened table (one or more rows per document).
- Request. Path:
template_id(resolved via synonym lookup). Query:status(defaultactive),page/page_size(default 1 / 100, max 1000),max_cross_product(default 1000, 1–10000). - Response.
TableViewResponse:template_id,template_value,template_label,columns[TableColumn](name,label,type,is_array,is_flattened),rows[],total_documents,total_rows,page,page_size,pages,array_handling(flattened|json|none). Array handling: 0 arrays → 1 row/document; 1 array → flattened; 2+ arrays with cross-product ≤max_cross_product→ full cross-product flatten; 2+ arrays over the cap → arrays kept as JSON string columns instead. Every row also carries underscore-prefixed metadata columns:_document_id,_version,_identity_hash,_status,_created_at,_updated_at. Pagination is by source document, so the actual row count on a page can exceedpage_sizeonce array flattening runs. - Errors. 404 if the template doesn't exist; 500 if the resolved template response is missing its
namespace. Read permission required on the template's namespace. - Example.
curl "$API/table/<template_id>?status=active" -H "X-API-Key: $KEY"
GET /table/{template_id}/csv
- Purpose. Export the same flattened table as a downloadable CSV.
- Request. Path:
template_id. Query:status(defaultactive),max_cross_product(default 1000),include_metadata(defaulttrue— include the underscore-prefixed columns). - Response.
text/csv,Content-Disposition: attachment; filename="<template value>.csv". No pagination — fetches every matching document. - Errors. Same as the JSON endpoint (404 template not found, 500 missing namespace).
- Example.
curl "$API/table/<template_id>/csv" -H "X-API-Key: $KEY" -o export.csv
Files
POST /files
-
Purpose. Upload a file to storage. Receives a Registry ID and is stored with status
orphanuntil a document references it. -
Request. Multipart:
file,namespace(required form field),file_id(optional pre-assigned, for restore/migration),description,tags(comma-separated),category,allowed_templates(comma-separated template values). -
Response.
FileResponse:file_id,namespace,filename,content_type,size_bytes,checksum,storage_key,metadata,status,reference_count,allowed_templates,uploaded_at,uploaded_by,updated_at,updated_by. -
Errors. 503 if file storage isn't enabled. 413 if content exceeds the configured max upload size. 400 on an empty file. The upload's content type is checked against an allow-list before storage; a disallowed type is rejected. Write permission required on
namespace.⚠ GAP: the configured max upload size and the upload content-type allow-list are enforced from
..main.settingsandservices/file_validation.pyrespectively — neither file is in this doc'ssource_scope, so the concrete limit/allow-list values aren't stated here. -
Example.
curl -X POST "$API/files" -H "X-API-Key: $KEY" -F "namespace=<namespace>" -F "file=@./report.pdf"
GET /files
- Purpose. List files with filtering and pagination.
- Request. Query:
namespace(omit for all accessible),status,content_type(supports glob, e.g.image/*),category,tags(comma-separated, all must match),uploaded_by,page/page_size(default 1 / 20, max 100). - Response.
FileListResponse:items[FileResponse],total,page,page_size,pages. - Errors. —
- Example.
curl "$API/files?namespace=<namespace>&status=active" -H "X-API-Key: $KEY"
GET /files/{file_id}
- Purpose. Get file metadata by ID.
- Request. Path:
file_id. Query:namespace. - Response.
FileResponse. - Errors. 404 if not found. Read permission required.
- Example.
curl "$API/files/<file_id>" -H "X-API-Key: $KEY"
GET /files/{file_id}/download
- Purpose. Get a pre-signed URL for direct file download.
- Request. Path:
file_id. Query:namespace,expires_in(seconds, 60–86400, default 3600). - Response.
FileDownloadResponse:file_id,filename,content_type,size_bytes,download_url,expires_in. - Errors. 404 if not found.
- Example.
curl "$API/files/<file_id>/download?expires_in=600" -H "X-API-Key: $KEY"
GET /files/{file_id}/content
- Purpose. Stream the file content directly (no pre-signed URL).
- Request. Path:
file_id. Query:namespace. - Response. Streamed binary body,
Content-Disposition: attachment,Content-Lengthset fromsize_bytes,media_typefrom the storedcontent_type. - Errors. 404 if not found; 400 if the file's status is
inactive("File has been deleted"). Read permission required. - Example.
curl "$API/files/<file_id>/content" -H "X-API-Key: $KEY" -o out.bin
PATCH /files
- Purpose. Update metadata for one or more files.
- Request. Body: array of
UpdateFileItem(file_id+ optionaldescription,tags,category,custom,allowed_templates). Query:namespace. - Response.
DocumentBulkResponse-shaped (per-itemupdated/error). - Errors. Per-item error if the file doesn't exist. Write permission required per file's namespace.
- Example.
curl -X PATCH "$API/files" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '[{"file_id":"<file_id>","category":"invoice"}]'
DELETE /files
- Purpose. Soft-delete one or more files (sets status
inactive). - Request. Body: array of
DeleteItem(id,force— settrueto delete even if referenced by documents). Query:namespace. - Response. Bulk response, per-item
deleted/error. - Errors. Per-item error if the file doesn't exist. Write permission required.
- Example.
curl -X DELETE "$API/files" -H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '[{"id":"<file_id>"}]'
GET /files/{file_id}/documents
- Purpose. List active documents referencing a specific file (via the file-references index).
- Request. Path:
file_id. Query:namespace,page/page_size(default 1 / 10, max 100). - Response.
FileDocumentsResponse:items[FileDocumentRef](document_id,template_id,template_value,field_path,status,created_at),total,page,page_size,pages. - Errors. 404 if the file doesn't exist. Read permission required.
- Example.
curl "$API/files/<file_id>/documents" -H "X-API-Key: $KEY"
DELETE /files/{file_id}/hard
- Purpose. Permanently delete a file from storage and the database. Requires the file to already be soft-deleted (
status=inactive). - Request. Path:
file_id. Query:namespace. - Response.
{"status": "permanently_deleted", "file_id": "<file_id>"}. - Errors. 404 if not found; 400 on a service-level failure. Admin permission required (elevated over the soft-delete's write requirement).
- Example.
curl -X DELETE "$API/files/<file_id>/hard" -H "X-API-Key: $KEY"
GET /files/orphans/list
- Purpose. List files not referenced by any active document — a cleanup/maintenance query across all namespaces.
- Request. Query:
older_than_hours(0–720, default 0 = all orphans),limit(1–1000, default 100). - Response.
list[FileResponse]. - Errors. 403 unless the caller is superadmin.
- Example.
curl "$API/files/orphans/list?older_than_hours=24" -H "X-API-Key: $KEY"
GET /files/by-checksum/{checksum}
- Purpose. Find files with identical content (duplicate detection) within the caller's accessible namespaces.
- Request. Path:
checksum. - Response.
list[FileResponse]. A superadmin caller gets an unfiltered (all-namespace) search. - Errors. —
- Example.
curl "$API/files/by-checksum/<sha256>" -H "X-API-Key: $KEY"
GET /files/health/integrity
- Purpose. System-wide file storage integrity check.
- Request. No parameters.
- Response.
FileIntegrityResponse:status(healthy/warning/error),checked_at,summary(counts by issue type),issues[FileIntegrityIssue](type,severity,file_id,document_id,field_path,message). Detects orphan files, missing storage (record exists but content missing), and broken references (deleted files still referenced). - Errors. 403 unless the caller is superadmin.
- Example.
curl "$API/files/health/integrity" -H "X-API-Key: $KEY"
Import
POST /import/preview
- Purpose. Preview a CSV/XLSX file before importing — returns headers, sample rows, and detected format, so the caller can build a
column_mapping. - Request. Multipart:
file. Nonamespace— this endpoint only parses, it does not touch any namespace's data. - Response.
{"headers": [...], ...}on success (parser-detected shape);{"error": "<message>"}on an empty file or a parse failure. - Errors. No structured HTTP error status is raised for a parse failure — the response body carries
errorinstead, at HTTP 200. - Example.
curl -X POST "$API/import/preview" -H "X-API-Key: $KEY" -F "file=@./rows.csv"
POST /import
- Purpose. Import documents in bulk from a CSV/XLSX file, mapping columns to template fields.
- Request. Multipart:
file,template_id,column_mapping(JSON object,{"CSV Column Name": "template_field_name", ...}),namespace,skip_errors(defaultfalse). Term fields accept human-readable values in the CSV; they resolve to term IDs automatically. - Response.
{"total_rows", "succeeded", "failed", "skipped", "results": [...up to 100...], "errors": [...up to 50...]}. Rows are processed in batches of 50. - Errors.
{"error": ...}at HTTP 200 for a badcolumn_mapping, unparsable file, or columns referenced by the mapping that don't exist in the file. Withskip_errors=false(default), the import stops at the first failing row; withskip_errors=trueit continues and reports every row's outcome. Write permission required onnamespace. - Example.
curl -X POST "$API/import" -H "X-API-Key: $KEY" \ -F "file=@./rows.csv" -F "template_id=<template_id>" -F "namespace=<namespace>" \ -F 'column_mapping={"Name":"name","Email":"email"}'
Replay
POST /replay/start
- Purpose. Start a replay session that republishes stored documents as NATS events.
- Request. Body
ReplayRequest:filter(ReplayFilter:template_id,template_value,namespace— default"wip",status— default"active"),throttle_ms(0–5000, default 10),batch_size(10–1000, default 100). - Response.
ReplaySessionResponse:session_id,status,total_count,published,throttle_ms,message. Replayed events publish to a separate NATS streamWIP_REPLAY_{session_id}taggedmetadata.replay=true, so consumers can distinguish them from live events. - Errors. 400 on an invalid filter; 503 if the replay service can't start (e.g. streaming backend unavailable). Admin permission required on
filter.namespace— replay can trigger downstream side effects. - Example.
curl -X POST "$API/replay/start" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"filter":{"namespace":"<namespace>","status":"active"}}'
GET /replay/{session_id}
- Purpose. Get the current state of a replay session.
- Request. Path:
session_id. - Response.
ReplaySessionResponse. - Errors. 404 if the session doesn't exist; 500 if a found session has no namespace recorded (defensive — should not happen). Admin permission required on the session's recorded namespace.
- Example.
curl "$API/replay/<session_id>" -H "X-API-Key: $KEY"
POST /replay/{session_id}/pause
- Purpose. Pause a running replay session.
- Request. Path:
session_id. - Response.
ReplaySessionResponse. - Errors. 400 if the session isn't currently running. Admin permission required.
- Example.
curl -X POST "$API/replay/<session_id>/pause" -H "X-API-Key: $KEY"
POST /replay/{session_id}/resume
- Purpose. Resume a paused replay session.
- Request. Path:
session_id. - Response.
ReplaySessionResponse. - Errors. 400 if the session isn't currently paused. Admin permission required.
- Example.
curl -X POST "$API/replay/<session_id>/resume" -H "X-API-Key: $KEY"
DELETE /replay/{session_id}
- Purpose. Cancel a replay session and delete its NATS stream.
- Request. Path:
session_id. - Response.
ReplaySessionResponse. - Errors. 404 if the session doesn't exist. Admin permission required.
- Example.
curl -X DELETE "$API/replay/<session_id>" -H "X-API-Key: $KEY"
Backup
POST /backup/namespaces/{namespace}/backup
- Purpose. Start a namespace (or multi-namespace) export job.
- Request. Path:
namespace(the anchor). BodyBackupRequest:namespaces(additional namespaces to add to the anchor),all_namespaces(defaultfalse— every registry namespace, overridesnamespaces),include_files,include_inactive,skip_documents,skip_closure,skip_synonyms,latest_only(all defaultfalse),template_prefixes,dry_run(defaultfalse). - Response. HTTP 202,
BackupJobSnapshot(job_id—"bkp-"+ 16 hex chars,kind,namespace,namespaces[],status,phase,percent,message,error, timestamps,archive_size,options,created_by). Export runs in a worker thread; the response returns immediately with the initial snapshot. - Errors. 400 if the resolved namespace set is empty. 409 if a job with this ID is already running in this worker. Admin permission required on every namespace in the resolved set.
- Example.
curl -X POST "$API/backup/namespaces/<namespace>/backup" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"include_files":true}'
POST /backup/namespaces/{namespace}/restore
- Purpose. Upload a backup archive and restore it into a namespace.
- Request. Path:
namespace(auth anchor / fallback target). Multipart:archive(the.zip),mode("restore"— the only implemented mode;"fresh"is rejected as not-yet-implemented, anything else is rejected as invalid),target_namespace(defaults to the pathnamespace),register_synonyms,skip_documents,skip_files,continue_on_error,dry_run(all defaultfalse),batch_size(1–500, default 50). The archive is streamed to disk in 1 MiB chunks — never buffered whole in memory. - Response. HTTP 202,
BackupJobSnapshot(job_id—"rst-"+ 16 hex chars). - Errors. 400 for an invalid
mode, amode="fresh"request, or an archive not on format v3 (the endpoint restores only v3 archives — the response includes a conversion hint viapython -m wip_toolkit convert-archive). 409 on a duplicate in-worker job. 500 if the upload fails to stream. For a v3 archive, admin permission is required on every namespace prefix its manifest declares; for a multi-namespace archive the per-namespacetarget_namespaceis left empty so the restore engine loops over each namespace in the archive itself. - Example.
curl -X POST "$API/backup/namespaces/<namespace>/restore" -H "X-API-Key: $KEY" \ -F "archive=@./backup.zip" -F "mode=restore"
GET /backup/jobs/{job_id}
- Purpose. Get the latest persisted snapshot for a backup/restore job.
- Request. Path:
job_id. - Response.
BackupJobSnapshot. - Errors. 404 if not found. Read permission required on
job.namespace. - Example.
curl "$API/backup/jobs/<job_id>" -H "X-API-Key: $KEY"
GET /backup/jobs
- Purpose. List recent backup/restore jobs.
- Request. Query:
namespace,status,limit(1–500, default 50). - Response.
list[BackupJobSnapshot], sorted bycreated_atdescending. Jobs the caller cannot read are silently dropped from the result rather than causing an error. - Errors. —
- Example.
curl "$API/backup/jobs?namespace=<namespace>" -H "X-API-Key: $KEY"
GET /backup/jobs/{job_id}/events
- Purpose. Server-Sent Events stream of job progress.
- Request. Path:
job_id. - Response.
text/event-stream. An initial: connectedcomment, then aprogressevent (JSONBackupProgressMessage:job_id,status,phase,percent,message,current,total,details) whenever the persisted job's status/phase/percent/message changes — polled every 500 ms from the persistedBackupJobrecord, so it works from any worker without session affinity. Terminates (returns) on a terminal status (complete/failed) or anerrorevent if the job disappears. Headers:Cache-Control: no-cache, no-transform,X-Accel-Buffering: no. The wire type is deliberatelyBackupProgressMessage, decoupled from whatever internal event type the backup engine emits. - Errors. 404 if the job doesn't exist (checked before the stream opens). Read permission required.
- Example.
curl -N "$API/backup/jobs/<job_id>/events" -H "X-API-Key: $KEY"
GET /backup/jobs/{job_id}/download
- Purpose. Download the archive produced by a completed backup job.
- Request. Path:
job_id. - Response. Streamed
application/zip(1 MiB chunks via a thread executor),Content-Disposition: attachment; filename="{namespace}-{job_id}.zip",Content-Lengthset. - Errors. 404 if the job doesn't exist; 400 if the job is a restore job (no archive to download); 409 if the job isn't
complete; 500 if the job has noarchive_pathrecorded; 410 if the archive file is no longer on disk. Read permission required. - Example.
curl "$API/backup/jobs/<job_id>/download" -H "X-API-Key: $KEY" -o backup.zip
DELETE /backup/jobs/{job_id}
- Purpose. Delete a terminal job record and its archive file.
- Request. Path:
job_id. - Response. HTTP 204, no body.
- Errors. 404 if not found; 409 if the job is still
running("wait for it to finish"). Admin permission required. - Example.
curl -X DELETE "$API/backup/jobs/<job_id>" -H "X-API-Key: $KEY"
Health
GET /health
-
Purpose. Health check — verifies MongoDB, Registry, Template Store, and Def-Store connectivity (per
schemas/document-store.json's description of this route; tagHealth). -
Request. None.
-
Response. HTTP 200, unstructured body (no schema declared).
-
Errors. —
⚠ GAP: this route's handler is not under
components/document-store/src/document_store/**/api/— it is outside this doc'ssource_scope(declared only inschemas/document-store.json), so its implementation can't be verified beyond the schema's own summary/description text above.
Store-specific gotchas
forcemeans different things on documents vs. files, using the sameDeleteItemmodel. OnDELETE /documents, settingforce=trueis rejected outright (error_code=force_unsupported) — document deletion has no reference gate to override, since soft-delete keeps existing references resolving. OnDELETE /files,force=trueis exactly the reference-guard override it looks like: it deletes a file even if documents still reference it.PATCH /filesmetadata fields don't all merge the same way.custommerges with the existing value;description,tags,category, andallowed_templatesall replace the existing value outright.- Migration is identity-preserving only.
POST /documents/migratere-pins documents to a new template version without touching their data — but only if both template versions declare the sameidentity_fields. If they don't, the whole cohort is rejected withidentity_fields_changedrather than silently changing anyone's identity hash. - Relationship indexes are lazy. The Mongo indexes backing
GET /documents/{id}/relationshipsand.../traverse(on(template_id, data.source_ref)/(template_id, data.target_ref)) are created on the first relationship-document write, not at service startup. - Inactive relationship peers are not errors. With
?include=peers, a peer that is inactive is returned as a normal populatedpeerobject withstatus: "inactive"(for dimmed rendering);peer_error_codeis reserved for genuine resolution failures (not_found/forbidden). - Table view pagination is by source document, not by output row. Array-field flattening happens after the page is fetched, so the actual row count returned for a page can exceed
page_size. - Cursor-mode
GET /documentsdisables offset paging. Withcursorset,pageis ignored,totalcomes back as-1, andsort_by/sort_ordermust be omitted — cursor mode is strictly_id-ordered. - Restore only accepts format-v3 archives. An older or unreadable-manifest archive is rejected before any job or namespace is created, with a pointer to the
wip_toolkit convert-archiveCLI. - Two file-listing/integrity endpoints are superadmin-only, not namespace-admin.
GET /files/orphans/listandGET /files/health/integrityoperate across every namespace, so they require superadmin rather than a per-namespace admin check. GET /files/by-checksum/{checksum}scopes automatically. A non-superadmin caller is restricted to their accessible namespaces; only superadmin gets an unfiltered cross-namespace search.- Backup progress streaming has no worker affinity. The SSE endpoint polls the persisted
BackupJobrecord rather than reading an in-process queue, so it works correctly even if a different uvicorn worker than the one running the job serves the SSE connection.
