HTTP API · 1. Facts and memory

Forty-nine of the daemon's 347 route registrations read or write the memory substrate directly. This chapter documents all of them.

Planes covered: Facts · Substrate (entities, edges, kinds) · Relations graph · Memory (import, auto-capture, engrams) · Cases · Append and local prose ingest · Result-envelope import · Features lens · Tenant sync.

Read chapter 0 first for the auth model, the request-field notation (req, opt, dflt), the error shape and the ingress limits. This chapter does not repeat them.

Two things to know before you write anything:

  • A fact write is deliberate. Nothing on this plane captures automatically in the default configuration. The auto-capture routes in §1.6 are gated behind CORECRUXD_AUTO_CAPTURE, which is off by default, and even when on they produce candidates that a human or an agent must promote.
  • Deletion is a tombstone. Fact deletes write a superseding record rather than removing a row, so history remains reconstructable. Reads take the latest fact per entity-and-key pair.

1.1 Facts

The core key-value-with-provenance store. Nine registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/factsfacts.rs:639 query_factsany-of query:read, admin:read · Readquery: query (String, opt), entity (String, opt), entity_prefix (String, opt), top_k (usize, opt), token_budget (usize, opt), min_effective_confidence (f32, opt), as_of (String, opt)keys facts, total_tokens400, 401, 503-R
PUT/v1/factsfacts.rs:453 put_factany-of facts:write, admin:write · Writebody StoreFact: entity (String, req), key (String, req), value (String, req), tenant_hash (String, dflt default_tenant_hash), source_receipt (String, opt), confidence (f32, dflt default_confidence), private (bool, dflt), horizon_class (HorizonClass, dflt), actor (String, dflt)201, fact record401-W · facts
PUT/v1/facts/bulkfacts.rs:483 put_facts_bulkany-of facts:write, admin:write · Writebody: array of StoreFact as above201, keys facts400, 401, 500-W · facts
GET/v1/facts/entity/{entity}facts.rs:604 get_facts_by_entityany-of query:read, admin:read · Readpath entitykeys facts401-R
GET/v1/facts/exportfacts.rs:735 export_factsany-of query:read, admin:read · Readquery: since (String, opt), cursor (String, opt), limit (u32, opt)keys exported_at, facts, has_more, next_cursor401-R
GET/v1/facts/listfacts.rs:805 list_factsany-of query:read, admin:read · Readquery: cursor (String, opt), limit (usize, opt), include_reserved (String, opt), include_superseded (String, opt), entity_prefix (String, opt), q (String, opt), as_of_unix_ms (i64, opt)keys facts, has_more, limit, next_cursor, total_nondeleted, total_visible400, 401-R
GET/v1/facts/{factId}facts.rs:527 get_factany-of query:read, admin:read · Readpath factIdFact record401, 404-R
DELETE/v1/facts/{factId}facts.rs:561 delete_factany-of facts:write, admin:write · Writepath factIdkeys deleted401, 404, 500-W · facts
POST/v1/facts/aggregatefacts.rs:712 post_aggregateany-of query:read, admin:read · Writebody AggregateRequestV1: op (AggregateOp, req), entity (String, dflt), key (String, dflt), query (String, dflt), as_of (DateTime, dflt), token_budget (usize, dflt)Aggregate result-,R, mounted as a write-class POST

The scope correction you may be carrying forward. The repository's own docs/api-reference.md documents PUT /v1/facts, PUT /v1/facts/bulk and DELETE /v1/facts/{factId} as needing query:read. They are writes. require_fact_write_ctx requires any-of facts:write, admin:write (facts.rs:138), and the route-auth contract agrees. A token provisioned from that older document will work today under the default shadow mode and start failing the moment an operator sets CORECRUXD_ROUTE_AUTH=enforce. Provision write scopes for writes.

POST /v1/facts/aggregate is the mirror-image oddity: it reads, its handler checks read scopes, but because it is a POST the route-auth contract classifies it Write. Under enforce a caller therefore needs a write-class scope to run a read. That is the contract as shipped.

Privacy on the fact write path

PUT /v1/facts/bulk rejects private: true with a 400. Separately, fact_privacy::enforce forces private = true for reserved entity prefixes on every fact write path (mod.rs:440), including the ones that did not ask for it. A fact you wrote as public can come back marked private if its entity prefix is reserved. Check the stored record, not your request body.

1.2 Substrate: entities, edges and kinds

A typed object graph over the same store. Ten registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/entitiesentities.rs:58 list_entitiesany-of facts:read, admin:read · Readquery: kind (String, opt), limit (usize, opt), include_deleted (bool, dflt)keys count, entities-,R
GET/v1/entities/{kind}/{id}entities.rs:43 get_entityany-of facts:read, admin:read · Readpath kind, idkeys entity404-R
PUT/v1/entities/{kind}/{id}entities.rs:77 put_entityany-of facts:write, admin:write · Writepath kind, id; body: payload (Value, req)keys entity400-W
DELETE/v1/entities/{kind}/{id}entities.rs:114 delete_entityany-of facts:write, admin:write · Writepath kind, idkeys entity404-W
GET/v1/entities/{kind}/{id}/historyentities.rs:100 get_entity_historyany-of facts:read, admin:read · Readpath kind, idkeys count, versions-,R
GET/v1/edgesentities.rs:164 list_edgesany-of facts:read, admin:read · Readquery: from_kind (String, opt), from_id (String, opt), to_kind (String, opt), to_id (String, opt), edge_kind (String, opt), limit (usize, opt), include_deleted (bool, dflt)keys count, edges-,R
PUT/v1/edgesentities.rs:187 put_edgeany-of facts:write, admin:write · Writebody: from_kind (String, req), from_id (String, req), edge_kind (String, req), to_kind (String, req), to_id (String, req), payload (Value, dflt)keys edge400-W
DELETE/v1/edgesentities.rs:211 delete_edgeany-of facts:write, admin:write · Writebody: from_kind (String, req), from_id (String, req), edge_kind (String, req), to_kind (String, req), to_id (String, req)keys edge404-W
GET/v1/kindsentities.rs:236 list_kindsany-of facts:read, admin:read · Read-keys count, kinds-,R
GET/v1/kinds/{kind}entities.rs:246 get_kindany-of facts:read, admin:read · Readpath kindkeys registration404-R

DELETE /v1/edges takes a body, not query parameters. That is unusual and it is deliberate: an edge is identified by a five-part composite key.

1.3 Relations graph

A numeric artifact-id graph with confidence-weighted edges. Four registrations.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/relationsrelations.rs:126 get_relationsadmin:read · Readquery: tenant_id (String, req), from_id (u32, req)keys edges, from_id, to_id, edge_type, confidence, created_at_micros, updated_at_micros, tenant_id400-R
POST/v1/relationsrelations.rs:76 post_relationany-of facts:write, admin:write, tenant-bound · Writebody: tenant_id (String, req), from_id (u32, req), to_id (u32, req), edge_type (String, req), confidence (f32, dflt default_confidence), created_at_micros (i64, dflt), updated_at_micros (i64, dflt)Relation record400, 500-W
GET/v1/relations/incomingrelations.rs:166 get_incoming_relationsadmin:read · Readquery: tenant_id (String, req), to_id (u32, req), edge_type (String, dflt), cursor (String, dflt), limit (usize, dflt)keys edges, from_id, to_id, edge_type, confidence, cursor, next_cursor, limit, created_at_micros, updated_at_micros, tenant_id400-R
POST/v1/relations/expandrelations.rs:227 post_expandadmin:read · Writebody: tenant_id (String, req), seed_artifact_ids (Vec u32, req), edge_types (Vec String, dflt), max_hops (u32, dflt default_max_hops), budget (usize, dflt default_budget), min_confidence (f32, dflt)keys artifacts, artifact_id, hop_distance, score, stats, edges_traversed, edge_types_used, hops_used, budget_remaining400-R, mounted as a write-class POST

1.4 Append and local prose ingest

Two registrations. Both raise the ingress body limit; see chapter 0 §0.8.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/appendappend.rs:36 post_admin_appendadmin:write · Writebody: tenant_id (String, req), stream_type (String, req), stream_id (String, req), expected_next_seq (u64, dflt), events (Vec AppendEventBody, req)keys appended, stream_id400-W
POST/v1/local/ingestlocal_ingest.rs:200 post_local_ingestadmin:write, tenant-bound · AdminWritebody: tenant_id (String, req), corpus_id (String, req), documents (Vec LocalIngestDocument, req), semantic_profile (SemanticProfile, dflt)keys ingested, documents, frame_count, segment_seq, sealed, receipt_id, dense_dim, dense_vectors, capability, code, max_text_bytes, max_texts, max_total_text_bytes413, 422, 500CORECRUXD_LOCAL_INGEST, default onW · receipt

/v1/local/ingest serialises on a process-wide mutex (mod.rs:369) so two concurrent seals cannot race on the shard's exclusive storage handle. Concurrent callers queue; they do not fail. A long ingest can therefore make a second ingest appear to hang until the router's 30-second timeout returns 408.

The 413 here is the ingest's own size accounting (max_text_bytes, max_texts, max_total_text_bytes come back in the response so you can see which cap you hit), distinct from the ingress-layer 413.

1.5 Result-envelope import

One registration. Verifies a platform signature and a BLAKE3 content hash before importing.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/result-envelope/importresult_envelope.rs:96 post_result_envelope_importfacts:write, tenant-bound · Writebody ResultEnvelope: schema_version (String, req), job_id (String, req), tenant_id (String, req), payload (EnvelopePayload, req), blake3_content_hash (String, req), platform_signature (PlatformSignature, req), passport_fpr (String, dflt), credit_spend_receipt (String, dflt), companion_artifacts (Vec CompanionArtifact, dflt)keys status, imported_at, at, job_id, tenant_id, artefact_id, blake3_content_hash, companion_artifacts, counts, credit_spend_receipt, edges, entities, key_id, purpose_tag, reason, schema, sealed, size_bytes400, 403, 409, 500-W · receipt, facts

409 means the envelope has already been imported. 403 means the platform signature did not verify against the configured key. Neither is retryable without changing the request.

1.6 Memory: import, auto-capture and engrams

Eight registrations. Four of them are gated off by default.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/memory/importmemory_import.rs:79 post_memory_importany-of facts:write, admin:write, tenant-bound · Writebody: tenant_id (String, req), pack (CruxPack, req), dry_run (bool, dflt), principal_map (map String to String, dflt)ok, dry_run, pack_hash, pack_passport_fpr, imported_facts, collisions_superseded, skipped_duplicate_facts, imported_sessions, skipped_sessions, private_facts400, 401, 403, 404, 500CRUX_MEMORY_IMPORT, default offW · facts
POST/v1/memory/extractmemory_capture.rs:100 post_extractany-of facts:write, admin:write · Writebody: text (String, req), session_id (String, dflt), profile (String, dflt), session_date (String, dflt)schema, extracted, written, skipped_existing, candidates500CORECRUXD_AUTO_CAPTURE, default offW · receipt, facts
GET/v1/memory/candidatesmemory_capture.rs:183 get_candidatesany-of query:read, admin:read · Readquery: status (String, dflt)keys candidates, count, schema400CORECRUXD_AUTO_CAPTURE, default offR
POST/v1/memory/candidates/{id}/promotememory_capture.rs:226 post_promoteany-of facts:write, admin:write · Writepath id; body: reviewer (String, dflt), auto_threshold (f32, dflt)keys candidate_id, promoted_fact_id, schema, status404, 409, 422, 500CORECRUXD_AUTO_CAPTURE, default offW · facts
POST/v1/memory/candidates/{id}/rejectmemory_capture.rs:276 post_rejectany-of facts:write, admin:write · Writepath id; body: reason (String, req)keys candidate_id, schema, status404, 409, 500CORECRUXD_AUTO_CAPTURE, default offW · facts
GET/v1/engramsengrams.rs:71 list_engramsany-of query:read, admin:read · Readquery: intent_bucket (String, dflt)keys engrams, total, id, name, version, enabled, intent_bucket, query_pattern, capability_class_min, capability_class_max, generated_class, inherited_reason, policy_hash, prompt_hash, source_chunk_hashes, source_chunk_set_hash, created_at_unix_ms, schema-,R
POST/v1/memory/engrams/resolveengrams.rs:166 resolve_engramsany-of query:read, admin:read · Writebody: names (Vec String, dflt), tenant_id (String, dflt), tenant_id_camel (String, dflt), agent_id (String, dflt), agent_id_camel (String, dflt), manifest_hash (String, dflt), model_id (String, dflt), model_id_camel (String, dflt)keys engrams, name, version, content, engram_set_hash, manifest_hash, manifest_status, applicable_why, generated_class, inherited_reason, policy_hash, prompt_hash, receipt_id, receipt_linkage, source_chunk_hashes, source_chunk_set_hash, agent_id, tenant_id, schema403, 422-W · receipt
POST/v1/memory/session-initengrams.rs:122 memory_session_initany-of sessions:read, query:read, admin:read · Writebody: tenant_id (String, dflt), tenant_id_camel (String, dflt), agent_id (String, dflt), agent_id_camel (String, dflt), model_id (String, dflt), model_id_camel (String, dflt)keys body, capability_class, engram_manifest, hash, passport_id, schema, session_procedure, session_procedure_hash-,W

Auto-capture is gated extraction, not ambient capture. With CORECRUXD_AUTO_CAPTURE on, POST /v1/memory/extract produces candidates from text you hand it. Candidates are inert until promoted through /promote, and a rejection keeps the audit trail rather than deleting it. With the flag off, the default, all four routes return 404.

The tenant_id and tenant_id_camel pairs on the engram routes exist because both tenant_id and tenantId are accepted on the wire. Send one.

1.7 Cases (procedural memory)

Two registrations. A case is a task-action-outcome record with a reward, used for retrieval of prior procedure.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/casescases.rs:61 record_caseany-of facts:write, admin:write · Writebody: task (String, req), action (String, req), outcome (String, req), context (String, dflt), success (bool, dflt default_success), reward (f32, dflt default_reward), tags (Vec String, dflt), source_receipt (String, dflt)keys case400-W
POST/v1/cases/retrievecases.rs:78 retrieve_casesany-of query:read, admin:read · Readbody: task (String, req), top_k (usize, dflt default_retrieve_top_k), only_success (bool, dflt)keys cases-,R

The case store is injected as an axum extension at the innermost layer (mod.rs:1542), which is why these two routes are the only ones that need it.

1.8 Features lens (capability registry)

Seven registrations. A registry of capabilities across a portfolio, with dependency edges and audit records.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
GET/v1/features/capabilitiesfeatures.rs:84 list_capabilitiesany-of facts:read, admin:read · Readquery: system (String, opt), maturity (String, opt), audit (String, opt), promise (u64, opt), search (String, opt)keys count, items-,R
GET/v1/features/capabilities/{id}features.rs:98 get_capabilityany-of facts:read, admin:read · Readpath idCapability record404-R
GET/v1/features/capabilities/{id}/treefeatures.rs:113 get_dependency_treeany-of facts:read, admin:read · Readpath idkeys root, upstream, downstream404-R
GET/v1/features/capabilities/analysis/gapsfeatures.rs:185 analysis_gapsany-of facts:read, admin:read · Read-Gap analysis-,R
GET/v1/features/capabilities/analysis/promisesfeatures.rs:194 analysis_promisesany-of facts:read, admin:read · Read-Promise analysis-,R
GET/v1/features/capabilities/analysis/coveragefeatures.rs:203 analysis_coverageany-of facts:read, admin:read · Read-Coverage analysis-,R
POST/v1/features/capabilities/{id}/auditfeatures.rs:219 post_auditany-of facts:write, admin:write · Writepath id; body: status (String, req), auditor (String, opt), notes (String, opt)keys status, auditor, last_audited, notes400, 404-W

The three analysis/* paths are registered before {id}, so a capability whose id is literally analysis is unreachable. Do not mint one.

1.9 Tenant sync (peer to peer)

Six registrations. This is the daemon-to-daemon replication surface for a single tenant's collections.

MethodPathHandlerAuthRequestSuccess responseErrorsGateR/W
POST/v1/sync/handshake/noncesync.rs:318 post_handshake_noncepublic · Public-keys nonce, ttl_seconds404, 500-W
GET/v1/sync/tenants/{tenantId}/manifestsync.rs:435 get_tenant_manifestfacts:read, tenant-bound · Readpath tenantId; query: tenant_category (String, opt), owner_id (String, opt), membership_epoch (u64, opt), role_grants (String, opt)Manifest envelope-,R
GET/v1/sync/tenants/{tenantId}/collections/{collection}sync.rs:461 get_tenant_collectionfacts:read, tenant-bound · Readpath tenantId, collection; query: cursor (String, opt), limit (usize, opt), include_content (bool, dflt)Collection page400-R
POST/v1/sync/tenants/{tenantId}/promotions/previewsync.rs:487 post_promotion_previewfacts:read, tenant-bound · Writepath tenantId; body: allowlist (Vec String, dflt), include_content (bool, dflt), confirm_hash (String, dflt), records (Vec SyncCollectionRecord, dflt)Preview envelope-,R, mounted as a write-class POST
POST/v1/sync/tenants/{tenantId}/promotions/confirmsync.rs:504 post_promotion_confirmfacts:write, tenant-bound · Writepath tenantId; body: allowlist (Vec String, dflt), include_content (bool, dflt), confirm_hash (String, dflt), records (Vec SyncCollectionRecord, dflt)keys applied_count, record_hash, schema, tenant_id412-W · facts
POST/v1/sync/tenants/{tenantId}/offboardsync.rs:550 post_tenant_offboardfacts:write, tenant-bound · Writepath tenantId; body: membership_epoch (u64, dflt)Offboard result500-W · receipt, facts

The 412 on confirm is the whole point of preview. You call preview, you get a confirm_hash over the exact record set, you send it back with confirm. If the local state moved in between, confirm returns 412 and applies nothing. Do not retry confirm with the same hash, re-run preview.

POST /v1/sync/handshake/nonce is Public by contract and returns a nonce with a 120-second TTL (mod.rs:154). It is a write in the sense that it records the nonce; it discloses nothing.

When CORECRUXD_SYNC_MUTUAL_AUTH=1, default off, route-auth skips its scope check on the five /v1/sync/tenants/* templates entirely, because the handlers authorize them with an Ed25519 peer handshake instead. See chapter 0 §0.5.

1.10 Failure modes on this plane

SymptomMost likely cause
403 with missingAnyScope naming write scopes on a PUT /v1/factsA token provisioned from the old api-reference.md, which documented fact writes as query:read. See §1.1.
A fact you wrote as public reads back privatefact_privacy::enforce forces private = true for reserved entity prefixes on every write path (mod.rs:440).
400 on PUT /v1/facts/bulk with a body that works on PUT /v1/factsThe bulk route rejects private: true.
404 on /v1/memory/extract or /v1/memory/candidatesCORECRUXD_AUTO_CAPTURE is off. It is off by default.
404 on /v1/memory/importCRUX_MEMORY_IMPORT is off. It is off by default.
408 on a second /v1/local/ingest while a first is runningThe ingest mutex serialises seals; the second call waited past the 30-second router timeout.
412 on promotions/confirmLocal state changed after preview. Re-run preview for a fresh confirm_hash.
413 on /v1/append at well under 64 MiBYou are hitting the global 16 MiB cap, which means the request did not match the raised-limit path list exactly. Check for a trailing slash or a query string.

Sources