Skip to main content
Version: v0.9.0a2
Spec

Spec-03-HTTP-API

14 min readSpec contributor · SDK author · Adapter author25 HTTP routes

What this page is

Rendered compatibility entry point for Spec-03-HTTP-API. JSON/HTTP wire format for facts, peers, gardens, trust manifests, and capability tokens — every operation is JSON-over-HTTP so any language with an HTTP client can participate. No SDK required.

Authoritative source: spec/stigmem-spec-v0.9.0a1.md

Section body

Legacy §5 anchors are retained for existing links while the maintained route contract lives in Spec-03-HTTP-API.

Endpoint groups

Group
Sections
Purpose
Fact CRUD
§5.1–§5.5
Assert, query, retract, point lookup, node metadata.
Federation lifecycle
§5.6–§5.8, §5.11
Peer register, list, pull, push.
Conflict management
§5.9–§5.10
List + resolve.
Higher-order operations
§5.12–§5.13
Lint + synthesis.
Gardens
§5.14–§5.20
CRUD, membership, garden-filtered assert/query.
Federation trust
§5.21–§5.25
Manifests, capability tokens, quarantine.
Revisions before pre-reset draft: the pre-reset spec-draft, pre-reset draft, v1.0

From stigmem-spec-the pre-reset spec-draft.md:

This section defines the HTTP endpoints that constitute the Stigmem REST API. Every operation is expressed as a JSON-over-HTTP request so that any language with an HTTP client can participate — no SDK is required. Endpoints are grouped by function: fact CRUD (§5.1–§5.5), federation lifecycle (§5.6–§5.8, §5.11), conflict management (§5.9–§5.10), and higher-order operations (§5.12–§5.13).

From stigmem-spec-pre-reset draft.md:

§5.1–§5.13 unchanged from the pre-reset spec.

From stigmem-spec-v1.0.md:

§5.1–§5.13 unchanged from the pre-reset spec.

§5.1 Assert a fact

The primary write operation. Caller supplies the full (entity, relation, value, source, confidence, scope) tuple; the node stores it as an immutable record, stamping it with a server-generated id, wall-clock timestamp, and HLC value.

Facts are append-only.

This endpoint never overwrites an existing record — every call produces a new row, even if the same tuple was asserted before.

POST /v1/facts
{ "entity": "stigmem://company.example/user/alice", "relation": "memory:role",
"value": { "type": "string", "v": "CEO" },
"source": "stigmem://company.example/agent/assistant", "confidence": 1.0, "scope": "company" }
→ 201 { "id": "<uuid>", "timestamp": "...", "hlc": "...", ...fact... }

§5.2 Query facts

The primary read path. Filter by any combination of entity, relation, source, scope; the node returns matching facts sorted by recency. Cursor-paginated for large result sets. By default excludes contradicted and expired facts.

GET /v1/facts?entity=stigmem://company.example/user/alice&relation=memory:role
→ 200 { "facts": [...], "total": 1, "cursor": null }

Query params: entity, relation, source, scope, min_confidence, after, include_contradicted, include_expired, cursor, limit.

§5.3 Node metadata

Every node exposes a discovery document at the well-known path so clients and peers can learn the node's capabilities before any authenticated call. This is the first endpoint a federation peer contacts during mutual discovery (§6.1).

GET /.well-known/stigmem
→ 200 {
"version": "0.8",
"node_id": URI,
"node_url": string,
"auth": "none" | "required",
"federation": "disabled" | "enabled",
"federation_pubkey": string, // pre-reset: base64url Ed25519 public key; omit if federation disabled
"federation_version": string, // pre-reset: semver range this node speaks, e.g. "0.8"
"federation_endpoints": { // pre-reset: advertised federation routes
"peers": string, // e.g. "/v1/federation/peers"
"facts": string, // e.g. "/v1/federation/facts"
"push": string | null // null if push not supported
},
"namespaces": ["memory:", "intent:", ...],
"spec": URI
}

Nodes with federation: "enabled" MUST populate federation_pubkey, federation_version, and federation_endpoints.

§5.4 Retract a fact

To retract a fact, assert a new fact for the same (entity, relation, scope) with confidence=0.0. The original fact is never deleted; the retraction is a new immutable entry.

POST /v1/facts
{ "entity": "stigmem://company.example/user/alice", "relation": "memory:role",
"value": { "type": "string", "v": "CEO" },
"source": "stigmem://company.example/agent/assistant", "confidence": 0.0, "scope": "company" }
→ 201 { ..., "confidence": 0.0 }

§5.5 Get a single fact

Retrieve a single fact by its server-assigned UUID. Useful for dereferencing fact IDs from other endpoints — inspecting conflict sides (§5.9), following fact_refs from a handoff payload (§4.6), or auditing a specific assertion.

GET /v1/facts/:id
→ 200 { ...fact... }
→ 404 if not found

§5.6 Register a peer — pre-reset

First step of the federation handshake. The calling node presents a signed declaration containing its identity (node_id), reachable URL, Ed25519 public key, and shared scopes.

POST /v1/federation/peers
Authorization: Bearer <api-key with federate permission>
{
"node_url": "https://node-b.example.com",
"node_id": "stigmem://node-b.example.com",
"federation_pubkey": "<base64url Ed25519 pubkey>",
"allowed_scopes": ["public"],
"declaration_sig": "<base64url Ed25519 signature over canonical JSON of above fields + signed_at>",
"signed_at": "2026-05-02T00:00:00Z"
}
→ 201 {
"peer_id": "<uuid>",
"status": "pending_verification",
"verified_at": null
}

Verification flow:

  1. Receiving node fetches {node_url}/.well-known/stigmem to retrieve the peer's federation_pubkey.
  2. Node verifies declaration_sig over the canonical JSON payload.
  3. If valid, peer status transitions to "active". If federation_pubkey mismatches the one at /.well-known/stigmem, status becomes "rejected".
  4. Mutual federation requires both sides to register each other. A node MAY auto-register a reciprocal peer declaration; it MUST NOT begin replicating until both sides are "active".

§5.7 List peers — pre-reset

Returns the set of federation peers known to this node. Operators audit federation topology; tooling confirms mutual registration before triggering replication.

GET /v1/federation/peers
→ 200 { "peers": [{ "peer_id": "...", "node_id": "...", "node_url": "...",
"status": "active" | "pending_verification" | "rejected" | "revoked",
"allowed_scopes": [...], "established_at": "..." }] }

§5.8 Pull replication — pre-reset

Default mechanism for synchronising facts between federated peers. Opaque cursor + limit; response returns facts created or received after the cursor's HLC position, filtered to authorised scopes.

GET /v1/federation/facts?scope=public&cursor=<cursor>&limit=100
Authorization: Bearer <peer-token>
→ 200 {
"facts": [...], // facts since cursor; max limit=500
"cursor": "<opaque>", // new cursor; persist this for next call
"has_more": true | false
}

Cursor semantics

An opaque string encoding the HLC of the last fact returned. Stable: re-requesting the same cursor returns the same facts (idempotent). null requests from the beginning.

Scope filtering

The peer token's scopes claim restricts which scopes can be returned. Nodes MUST NOT return facts outside the intersection of allowed_scopes in the PeerDeclaration and the token's scopes claim.

Idempotency

Nodes MUST deduplicate by fact id. Re-ingesting a fact that already exists locally is a silent no-op.

§5.9 List conflicts — pre-reset

When two facts for the same (entity, relation, scope) tuple arrive with conflicting values (whether local or federated), the node records them as a conflict (§7). Filterable by status; each entry carries references to both original facts.

GET /v1/conflicts?status=unresolved&cursor=<cursor>&limit=50
→ 200 {
"conflicts": [{
"conflict_id": "stigmem:conflict:<uuid>",
"fact_a": { ...fact... },
"fact_b": { ...fact... },
"status": "unresolved" | "resolved",
"resolved_by": "<fact-id>" | null,
"detected_at": "ISO8601"
}],
"cursor": "...",
"has_more": false
}

§5.10 Resolve a conflict — pre-reset

Explicit human- or agent-driven decision that picks a winner, optionally supplies a fresh reconciliation value, and records the rationale as auditable facts.

POST /v1/conflicts/:conflict_id/resolve
Authorization: Bearer <api-key>
{
"winning_fact_id": "<fact-id>", // one of the two conflicting facts, OR null
"resolution_note": "string", // human-readable rationale; stored as fact
"new_value": { FactValue } // optional: assert a fresh reconciliation value
}
→ 200 {
"resolution_fact_id": "<uuid>", // the new fact that captures the resolution
"conflict_status": "resolved"
}

Resolution semantics: The node asserts:

  1. A new fact for (entity, relation, scope) with the winning or new value and confidence=1.0.
  2. A stigmem:resolves meta-fact: (entity=<resolution-fact-id>, relation="stigmem:resolves", value={type:"ref", v:"<conflict_id>"}, source=<caller's entity_uri>, ...).
  3. Updates the conflict's stigmem:conflict:status to "resolved".

Both original conflicting facts remain immutable in the store.

§5.11 Push replication (optional) — pre-reset

Opt-in, low-latency complement to pull. The producing node forwards facts to a peer as soon as they are committed. Guarded by STIGMEM_FEDERATION_PUSH_ENABLED (default false).

POST /v1/federation/facts/push
Authorization: Bearer <peer-token>
{ "facts": [...], // array of FactObject as they would appear in GET /v1/facts responses
"sender_hlc": "<hlc>" }
→ 202 { "accepted": <int>, "rejected": <int>, "errors": [...] }

Push is opt-in. Nodes that do not support push SHOULD return 405. Implementations SHOULD prefer pull; push is provided for low-latency delivery behind a feature flag.

§5.12 Lint — pre-reset normative

The lint endpoint runs a configurable set of data-quality checks against a scope and returns machine-readable findings. Diagnostic counterpart to the decay sweeper (§15). See §14 for the full finding schema and check catalogue.

POST /v1/lint
Authorization: Bearer <api-key>
{ "scope": "company", "checks": ["contradiction", "stale"] }
→ 200 { "findings": [...], "checked_at": "...", "scope": "company",
"checks_run": ["contradiction","stale"], "fact_count": 142 }

§5.13 Synthesize scope — pre-reset draft

Produces a confidence-weighted summary of everything a scope knows about a given entity (or the entire scope when no entity filter is supplied). Collapses the timeline: for each (entity, relation) pair, surfaces the single highest-confidence live value, flags active contradictions, reports total fact count.

POST /v1/synthesis
Authorization: Bearer <api-key>
{ "scope": "company", "entity": "<optional-uri>", "min_confidence": 0.5 }
→ 200 { "summary": [...], "synthesized_at": "...", "scope": "company",
"fact_count": 142, "contradiction_count": 3 }

§5.14 Create a garden

POST /v1/gardens
Authorization: Bearer <api-key with write permission>
{
"slug": "project-atlas", // URL-safe identifier; unique within the node
"name": "Project Atlas", // display name
"scope": "company", // facts in this garden must have this scope
"description": "Atlas project memory." // optional
}
→ 201 {
"id": "<uuid>",
"garden_id": "stigmem://node.example.com/garden/project-atlas",
"slug": "project-atlas",
"name": "Project Atlas",
"scope": "company",
"description": "Atlas project memory.",
"created_by": "stigmem://node.example.com/agent/cto",
"created_at": "2026-05-03T00:00:00Z",
"members": [{
"entity_uri": "stigmem://node.example.com/agent/cto",
"role": "admin",
"added_by": "stigmem://node.example.com/agent/cto",
"added_at": "2026-05-03T00:00:00Z"
}]
}
→ 409 if slug already exists

The creating principal is automatically added as admin.

Slug rules: Must match ^[a-z0-9][a-z0-9\-]{0,62}$. Stored and matched case-insensitively.

Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.14 Create a garden — the pre-reset spec

POST /v1/gardens
Authorization: Bearer <api-key with write permission>
{
"slug": "project-atlas",
"name": "Project Atlas",
"scope": "company",
"description": "Atlas project memory."
}
→ 201 { ...GardenRecord with members... }
→ 409 if slug already exists

The creating principal is automatically added as admin. Slug rules: Must match ^[a-z0-9][a-z0-9\-]{0,62}$. Stored and matched case-insensitively.

§5.15 List gardens

GET /v1/gardens
Authorization: Bearer <api-key>
→ 200 { "gardens": [ ...GardenRecord... ] }

Returns only gardens where the caller holds any role (admin, writer, or reader). Admins of the node (callers with write permission) see all gardens.

Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.15 List gardens — the pre-reset spec

GET /v1/gardens
Authorization: Bearer <api-key>
→ 200 { "gardens": [ ...GardenRecord... ] }

Returns only gardens where the caller holds any role (admin, writer, or reader). Admins of the node (callers with write permission) see all gardens.

§5.16 Get a garden

GET /v1/gardens/:garden_id_or_slug
Authorization: Bearer <api-key>
→ 200 { ...GardenRecord with members... }
→ 403 if caller not a member
→ 404 if not found
Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.16 Get a garden — the pre-reset spec

GET /v1/gardens/:garden_id_or_slug
Authorization: Bearer <api-key>
→ 200 { ...GardenRecord with members... }
→ 403 if caller not a member
→ 404 if not found

§5.17 Delete a garden

DELETE /v1/gardens/:garden_id_or_slug
Authorization: Bearer <api-key>
→ 204
→ 403 if caller is not garden admin
→ 404 if not found

Deleting a garden does NOT delete its associated facts.

The garden_id field on orphaned facts becomes a dangling reference. Nodes SHOULD surface these in lint output (§14).

Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.17 Delete a garden — the pre-reset spec

DELETE /v1/gardens/:garden_id_or_slug
Authorization: Bearer <api-key>
→ 204
→ 403 if caller is not garden admin
→ 404 if not found

Deleting a garden does NOT delete its associated facts. The garden_id field on orphaned facts becomes a dangling reference. Nodes SHOULD surface these in lint output (§14).

§5.18 Garden membership

Add member:

POST /v1/gardens/:garden_id_or_slug/members
Authorization: Bearer <api-key> (must be garden admin)
{
"entity_uri": "stigmem://node.example.com/user/alice",
"role": "writer" // "admin" | "writer" | "reader"
}
→ 201 { ...GardenMemberRecord... }
→ 403 if caller is not garden admin
→ 404 if garden not found
→ 409 if entity already a member (use PATCH to change role)

Update member role:

PATCH /v1/gardens/:garden_id_or_slug/members/:entity_uri
Authorization: Bearer <api-key> (must be garden admin)
{ "role": "reader" }
→ 200 { ...GardenMemberRecord... }
→ 403 if caller is not garden admin or is demoting themselves (must retain at least one admin)

Remove member:

DELETE /v1/gardens/:garden_id_or_slug/members/:entity_uri
Authorization: Bearer <api-key> (must be garden admin)
→ 204
→ 403 if caller would remove the last admin

List members:

GET /v1/gardens/:garden_id_or_slug/members
Authorization: Bearer <api-key> (must be any member)
→ 200 { "members": [ ...GardenMemberRecord... ] }
Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.18 Garden membership — the pre-reset spec

(Same as above — identical request/response shapes for add/update/remove/list.)

§5.19 Assert a fact into a garden

Facts are associated with a garden by including garden_id in the assert request.

POST /v1/facts
Authorization: Bearer <api-key with write permission>
{
"entity": "stigmem://node.example.com/project/atlas",
"relation": "roadmap:status",
"value": { "type": "string", "v": "in-flight" },
"source": "stigmem://node.example.com/agent/cto",
"scope": "company",
"garden_id": "stigmem://node.example.com/garden/project-atlas"
}
→ 201 { ...FactRecord..., "garden_id": "...", "attested": true }
→ 403 if caller is not a writer or admin in the garden
→ 403 if scope does not match the garden's declared scope
→ 404 if garden not found
Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.19 Assert a fact into a garden — the pre-reset spec

(Same shape as above.)

§5.20 Query facts with garden filter

GET /v1/facts?garden_id=stigmem://node.example.com/garden/project-atlas
→ 200 { "facts": [...], "total": N, "cursor": "..." }
→ 403 if caller is not a member of the garden

Garden ACL is enforced.

The garden_id query parameter is additive with other filters (entity, relation, scope, etc.). Non-members get 403, not an empty result, to prevent membership enumeration.

Revisions before v1.0: pre-reset draft

From stigmem-spec-pre-reset draft.md:

5.20 Query facts with garden filter — the pre-reset spec

(Same shape as above; garden ACL is enforced — non-members get 403 not empty result.)

§5.21 Publish an org manifest

Bootstrap step for the federation trust model (§19). The admin uploads a self-signed manifest that declares the node's public key and the entity URIs it speaks for. The node verifies the signature against the embedded public key, stores the manifest, and (if configured) submits it to the transparency log (§19.2).

PUT /v1/federation/manifest
Authorization: Bearer <admin api-key>
Content-Type: application/json

{
"manifest_version": 1,
"entity_uri": "stigmem://company.example", // root entity URI for this node/org
"public_key": "<base64url Ed25519 public key>", // active signing key
"key_id": "<sha256 fingerprint of public key>",
"entities": [ // entity URIs this manifest is authoritative for
"stigmem://company.example/agent/assistant",
"stigmem://company.example/adapter/hook"
],
"rotation_events": [], // see §19.1.4; empty on first publish
"issued_at": "2026-05-04T00:00:00Z",
"expires_at": "2027-05-04T00:00:00Z",
"signature": "<base64url Ed25519 sig over canonical JSON>"
}
→ 200 { "manifest_id": "...", "log_entry_url": "..." }
→ 400 if signature verification fails or required fields missing
→ 403 if caller not admin

§5.22 Resolve an org manifest

Peers call this endpoint during the federation handshake to retrieve the manifest for a given entity URI. The response contains the full manifest object — giving the peer everything it needs to verify capability tokens (§19.3) issued by this node.

GET /v1/federation/manifest/:entity_uri_encoded
→ 200 { ...manifest object... }
→ 404 if no manifest found for entity_uri

§5.23 Issue a capability token

Capability tokens (§19.3) are short-lived, scoped credentials that replace static API keys for inter-node operations. This endpoint mints a signed token granting the named subject a specific verb on a specific object. Self-contained — the receiving peer verifies it using the issuer's manifest public key without calling back.

POST /v1/federation/capability-tokens
Authorization: Bearer <admin api-key>

{
"issuer": "stigmem://company.example",
"subject": "stigmem://company.example/agent/assistant",
"verb": "write",
"object": "stigmem://partner.example/scope/shared",
"expiry": "2026-06-01T00:00:00Z",
"nonce": "<32-byte hex random>"
}
→ 201 { "token": "<base64url-encoded signed JWT-like structure>", "token_id": "..." }
→ 403 if caller not admin

§5.24 Revoke a capability token

Revocation invalidates a previously-issued token before its natural expiry. Recorded in the local revocation list and submitted to the transparency log (§19.2.5) so peers can independently verify the revocation without trusting the issuing node's runtime state.

POST /v1/federation/capability-tokens/:token_id/revoke
Authorization: Bearer <admin api-key>

{} // empty body; revocation event is logged to transparency log
→ 204
→ 404 if token_id not found

§5.25 Quarantine garden operations

Facts that arrive from untrusted or low-scoring federation sources land in a quarantine garden (§19.7) rather than the target scope. These operations let a moderator review quarantined facts and either promote them or reject them permanently. Both actions are auditable.

Promote a fact from quarantine to a target garden:

POST /v1/gardens/:quarantine_garden_id/promote
Authorization: Bearer <api-key> (must hold quarantine:moderator role)

{
"fact_id": "<uuid>",
"target_garden_id": "<uuid or null for no-garden>",
"reason": "Verified provenance."
}
→ 200 { "fact_id": "...", "promoted_at": "...", "promoted_by": "..." }
→ 403 if caller lacks quarantine:moderator
→ 404 if fact_id not found in quarantine garden
→ 409 if fact already promoted or rejected

Reject a quarantined fact:

POST /v1/gardens/:quarantine_garden_id/reject
Authorization: Bearer <api-key> (must hold quarantine:moderator role)

{
"fact_id": "<uuid>",
"reason": "Failed source attestation; untrusted origin."
}
→ 200 { "fact_id": "...", "rejected_at": "...", "rejected_by": "..." }
→ 403 if caller lacks quarantine:moderator
→ 404 if fact_id not found in quarantine garden
→ 409 if fact already promoted or rejected