API Reference
This is the complete wire-level reference. Authentication is a bearer key on every request: Authorization: Bearer <key>, created in the dashboard. Every response arrives in one envelope: {"ok": true, "data": …, "request_id": "…"} on success, and on failure {"ok": false, "error": {"code", "message", "details"}, "request_id"} with a matching HTTP status — quote the request_id when reporting a problem. Every request names its project in the X-Hivemind-Project header, and a request without it is refused with 400: there is no default project for a key. A key reaches every project of the workspace, or the projects that list its role, and a request naming a project the key does not reach is refused with 403. A key also carries what it may remove — nothing, the recoverable deletes, or those plus the permanent purges — and a new key starts able to read and write only; a route the key's level does not allow answers 403. The conceptual documentation — the context window model, recall and ranking, budget mechanics — lives in the main documentation. GET /health on the host root answers {"status": "ok"} without authentication — use it for liveness checks. Routes under /operator/, /metrics, and /diagnostic/ are service-internal and refuse customer keys.
Client setup
Every example on this page uses three helpers — one POST, one GET, one DELETE — defined once per language. They carry the base URL, the bearer key, the project, and JSON encoding; every endpoint call is then a single short expression. The base URL is the host root: clients append /hivemind/v1 exactly once. The project is sent as the X-Hivemind-Project header, which every request must carry: the id of the project to work in, shown beside each project in the dashboard.
BASE="https://api.hivemind.militant.ai/hivemind/v1"
export HIVEMIND_API_KEY="sk_your_key"
export HIVEMIND_PROJECT="proj_your_project"
# every call: curl with the Authorization and X-Hivemind-Project headers, JSON in, JSON outconst BASE = "https://api.hivemind.militant.ai/hivemind/v1";
const HEADERS = {
"Authorization": `Bearer ${process.env.HIVEMIND_API_KEY}`,
"X-Hivemind-Project": process.env.HIVEMIND_PROJECT,
"Content-Type": "application/json",
};
async function hm(path, body) {
const res = await fetch(BASE + path, { method: "POST", headers: HEADERS, body: JSON.stringify(body) });
const data = await res.json();
if (!data.ok) throw new Error(`${res.status}: ${JSON.stringify(data.error)}`);
return data;
}
async function hmGet(pathWithQuery) {
const res = await fetch(BASE + pathWithQuery, { headers: HEADERS });
const data = await res.json();
if (!data.ok) throw new Error(`${res.status}: ${JSON.stringify(data.error)}`);
return data;
}
async function hmDelete(path) {
const res = await fetch(BASE + path, { method: "DELETE", headers: HEADERS });
const data = await res.json();
if (!data.ok) throw new Error(`${res.status}: ${JSON.stringify(data.error)}`);
return data;
}import os
import requests
BASE = "https://api.hivemind.militant.ai/hivemind/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIVEMIND_API_KEY']}",
"X-Hivemind-Project": os.environ["HIVEMIND_PROJECT"],
}
def hm(path, body):
response = requests.post(BASE + path, headers=HEADERS, json=body, timeout=120)
data = response.json()
if not data.get("ok"):
raise RuntimeError(f"{response.status_code}: {data.get('error')}")
return data
def hm_get(path_with_query):
response = requests.get(BASE + path_with_query, headers=HEADERS, timeout=120)
data = response.json()
if not data.get("ok"):
raise RuntimeError(f"{response.status_code}: {data.get('error')}")
return data
def hm_delete(path):
response = requests.delete(BASE + path, headers=HEADERS, timeout=120)
data = response.json()
if not data.get("ok"):
raise RuntimeError(f"{response.status_code}: {data.get('error')}")
return datapackage main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
)
const base = "https://api.hivemind.militant.ai/hivemind/v1"
func jsonBody(raw string) []byte { return []byte(raw) }
func do(req *http.Request) map[string]any {
req.Header.Set("Authorization", "Bearer "+os.Getenv("HIVEMIND_API_KEY"))
req.Header.Set("X-Hivemind-Project", os.Getenv("HIVEMIND_PROJECT"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer res.Body.Close()
var out map[string]any
json.NewDecoder(res.Body).Decode(&out)
if ok, _ := out["ok"].(bool); !ok { log.Fatalf("%d: %v", res.StatusCode, out["error"]) }
return out
}
func hm(path string, body []byte) map[string]any {
req, _ := http.NewRequest("POST", base+path, bytes.NewReader(body))
return do(req)
}
func hmGet(pathWithQuery string) map[string]any {
req, _ := http.NewRequest("GET", base+pathWithQuery, nil)
return do(req)
}
func hmDelete(path string) map[string]any {
req, _ := http.NewRequest("DELETE", base+path, nil)
return do(req)
}Client implementation guide
A correct client holds two pieces of state and follows six rules. Everything else on this page is per-endpoint detail.
The state
A turn counter, incremented once per exchange within a session. The open exchange: after a /turn succeeds, remember its exchange_pair_id and turn number until the reply is stored or the generation is abandoned. No conversation history: the service stores every message and reads the session’s recent conversation back on every /turn, newest first, as much as budget_total can hold, in reading order.
The lifecycle of one exchange
Increment the turn counter. Generate a fresh exchange_pair_id — a time-ordered UUIDv7 by convention. Call /turn with the user message as user_input. Run your model on data.bundle.messages. Stream or return the reply to your user. Then call /conversation/store with the reply, the same exchange pair id and turn number, role "assistant", and author in additional_metadata if you attribute agents. The exchange is complete.
Streaming hosts
/turn performs a store, two semantic recalls, and a full compile — at large budgets that is a multi-second operation. Establish your output stream first (send headers or a keep-alive frame), then call /turn, then stream the model. Otherwise streaming clients treat the absence of bytes before the compile finishes as a failed connection.
Restarts
Nothing to rebuild: the conversation lives in the service. Keep your conversation id mapped to session_id so the restarted host resumes the same session, and when a user clears a thread, start a new session id rather than rebuilding the old one.
Aborted generation
If the user cancels mid-stream or sends a new message before the reply finished, make no Hivemind call. Discard the open exchange. The user message was already stored by /turn and stands as an unanswered call; the next message is a new turn. Never store empty content, and never store a partial reply as cleanup — the only legitimate partial is a host that deliberately keeps what was shown on screen as what was said, decided after the stream has ended.
Retries and backoff
Never re-send a /turn that timed out: the request may have executed, and replaying it stores the message twice. For a /conversation/store that timed out, verify first: replay the session with /conversation/recall and look for the reply under its exchange_pair_id, then re-send only if it is not there. Retry only requests that failed before any response began (connection refused, DNS failure). On a 429, sleep exactly the number of seconds in the Retry-After header and retry — that is the service asking your client to slow down. On a 5xx or a timeout, decide once whether your host fails open (proceed with local context only) or fails closed, and document the choice.
Content rules
Text is stored and returned verbatim. Recalled text re-enters a model’s context on later turns, so strip or alter model control tokens (markers of the <|…|> kind) before storing. Size budget_total as your model window minus whatever you wrap around the packet, and read your context meter from the response’s token_count against budget_total — counting your own messages undercounts, and your model provider counts a different tokenizer after your additions.
POST /turn
The compile side of one exchange, in a single request. The service stores user_input as the call half of a new exchange (receipted), runs semantic recall over deliberate memories and past conversation using the input as the query, reads the session’s recent conversation back from what it stored (newest first, as much as budget_total can hold), and compiles them with active holds and the operator briefing into one token-budgeted prompt packet, user_input last. Recalled conversation that is already in the recent conversation is left out. Your model runs on the packet; POST /conversation/store then records the reply, completing the exchange. Degraded recall or a failed store surface in warnings — the turn still succeeds.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
user_input | string | yes | The initiating message of this exchange. Stored verbatim and used as the recall query. |
session_id | string | yes | Your conversation identifier. Map your own thread id to it so a restarted host resumes the same session; start a new id when a user clears a thread. |
turn_number | integer | yes | Monotonic per session; your client increments it each turn. |
budget_total | integer | yes | Token budget for the compiled packet. Maximum 128,000; larger values are rejected with a 400 naming the limit. The budget bounds what is surfaced per turn, not what is remembered. |
exchange_pair_id | string | yes | A fresh collision-resistant id per exchange; the store call must repeat it exactly. Convention: time-ordered UUIDv7. |
messages | array of objects | no | Only what the service does not store and later compiles should see — tool results — never the conversation itself, which the service reads back. ONE of two shapes — never mixed in one request. Chat form: {role, content, timestamp, metadata?}, role user | assistant | system | tool, with a real unix timestamp — the compiler orders conversation by timestamp. A role: "tool" entry carries a tool result; put {"tool_call_id", "tool_name"} in its metadata and the packet renders them as the message's tool_call_id and name. Event form (detected when the first entry has type and payload): each entry {type, payload, id?, ts?} with types user.input (payload.text), assistant.message.final (payload.content), and tool.call.finished (payload.call_id, ok, result | error) — tool events land in the dedicated tool admission lane governed by max_tool_items/include_tools. |
operator_briefing | string | no | Standing instructions compiled into the packet. Never stored; re-send it every turn. |
holds | array of objects | no | Pinned operational state, each {id, title, content}. Compiled into every packet you send them with; never stored, never recalled, never receipted. Your client keeps the list and drops entries when the condition clears. |
artifacts_context | string | no | One-shot context (a document excerpt, a tool result) compiled into this packet only. |
recall_top_k | integer | no | Forces a fixed recall pool. Omit it: the service sizes the pool from the budget, and your subscription caps the maximum. |
author | string | no | Who produced this call — identity metadata, orthogonal to role. Surfaces as the chat message name in compiles and recalls, and filters in metadata recall. |
policy | object | no | Compiler policy overrides, applied as an overlay: fields you omit keep their budget-derived values (window from budget_total, balanced admission, budget-scaled caps). Omit entirely unless you need a cap or flag. |
config | object | no | Advanced compiler configuration. Omit unless you know you need it. |
Example
curl -s -X POST "$BASE/turn" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"user_input": "Can we ship the migration tonight?",
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"operator_briefing": "You are the deployments assistant. Be direct.",
"holds": [
{
"id": "stop-order",
"title": "stop-order",
"content": "No production deploys until the incident review closes."
}
],
"author": "operator-7"
}'const result = await hm("/turn", {
"user_input": "Can we ship the migration tonight?",
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"operator_briefing": "You are the deployments assistant. Be direct.",
"holds": [
{
"id": "stop-order",
"title": "stop-order",
"content": "No production deploys until the incident review closes."
}
],
"author": "operator-7"
});
const packet = result.data.bundle.messages; // feed this to your modelresult = hm("/turn", {
"user_input": "Can we ship the migration tonight?",
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"operator_briefing": "You are the deployments assistant. Be direct.",
"holds": [
{
"id": "stop-order",
"title": "stop-order",
"content": "No production deploys until the incident review closes."
}
],
"author": "operator-7"
})
packet = result["data"]["bundle"]["messages"] # feed this to your modelresult := hm("/turn", jsonBody(`{
"user_input": "Can we ship the migration tonight?",
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"operator_briefing": "You are the deployments assistant. Be direct.",
"holds": [
{
"id": "stop-order",
"title": "stop-order",
"content": "No production deploys until the incident review closes."
}
],
"author": "operator-7"
}`))
// result["data"].bundle.messages feeds your modelResponse
bundle.messages is a contiguous, internally ordered block — use it as the whole prompt or wrap it above and below, but never insert into or reorder inside it. stored confirms the call half was written; session_history counts the stored messages of this session read back as the recent conversation; timings is the server-side phase breakdown in seconds.
{
"ok": true,
"data": {
"bundle": {
"messages": [
{
"id": "…",
"role": "system",
"content": "…compiled packet: briefing, holds, recalled memory, conversation…"
},
{
"id": "01a047c7-4c0c-73a0-80d9-885d20403920",
"role": "user",
"content": "Can we ship the migration tonight?",
"name": "operator-7"
}
],
"policy": {
"max_context_tokens": 8192, "max_items": 64, "max_memory_items": 16,
"max_tool_items": 10, "max_hold_items": null, "include_memory": true,
"include_tools": true, "include_system": true, "include_directives": true,
"include_holds": true, "admission_strategy": "balanced"
},
"token_count": 3210,
"source_counts": {
"conversation": 13,
"hivemind": 4,
"recalled_conversation": 2,
"directive": 1,
"system": 3
},
"budget_total": 8192,
"budget_available": 4982,
"conversation_tokens": 310,
"hold_tokens": 84,
"trace": {
"trace_id": "…",
"event_id": "…",
"receipt_id": "…",
"parent_receipt_id": null
}
},
"receipt": {
"receipt_id": "01a0a64b-28b3-7e9b-a152-70ec1f4b6a29",
"operation": "context.compile",
"status": "ok",
"created_at": "2026-08-26T01:20:11.412331+00:00",
"tenant_id": "hm_tenant_9f2c1a7b3d4e5f60",
"inputs_ref": {
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"recalled_conversation_count": 6,
"recalled_memory_count": 4
},
"outputs_ref": {
"token_count": 3210,
"item_ids": [
"…"
],
"source_counts": {
"conversation": 13,
"hivemind": 4,
"recalled_conversation": 2,
"directive": 1,
"system": 3
}
},
"source_receipt_ids": [],
"parent_receipt_id": null,
"request_id": "01a069b3-bbcb-7a6c-a9f5-ff21413e878a",
"idempotency_key": "01a069b3-bbcb-7a6c-a9f5-ff21413e878a"
},
"stored": {
"id": "01a047c7-4c0c-73a0-80d9-885d20403920",
"receipt_id": "01a0a7dc-f73a-745a-ab41-2a57f72bec14"
},
"recalled_memories": 4,
"recalled_conversation": 6,
"session_history": 12,
"warnings": [],
"timings": {
"embed_s": 0.025,
"store_and_recall_s": 1.703,
"completion_s": 0.317,
"compile_s": 0.005,
"receipts_s": 0.162,
"total_s": 2.212
}
},
"request_id": "01a056a5-6217-7a7c-8d3f-aa08b063b90a"
}Errors
400 VALIDATION_ERROR for an empty field or a budget above 128,000 (details.field names it); 400 "No storage connected" when no cluster is bound; 429 RATE_LIMITED with Retry-After; 401 for a bad key. A degraded sub-operation (one recall search failing) is a warnings entry, not an error. If your message cannot be stored the turn fails with 500 INTERNAL_ERROR and nothing is compiled: a message with no stored row would be missing from the conversation from then on.
POST /conversation/store
Stores one half of an exchange. In the standard loop it records the assistant reply, completing the exchange the last /turn opened; it is also how a custom loop stores either half itself. Recall returns whole exchanges, so completing pairs is what keeps answers attached to the questions that produced them.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
content | string | yes | The message text. Empty content is rejected. |
session_id | string | yes | Same session as the turn. |
turn_number | integer | yes | Same turn number as the turn. |
role | string | yes | "assistant" for the reply half, "user" for a call half. Role is positional — which side of the exchange — not human versus machine. |
exchange_pair_id | string | yes | Exactly the id the turn used. This is the pairing. |
additional_metadata | object | no | One convention matters: author attributes the half to a named agent or person. The question needs nothing here: /turn already stored it, and recall pairs the two by exchange_pair_id. Limits: at most 64 keys; a key up to 128 characters; a string value up to 8,000 characters; a list value up to 64 items. Anything beyond is rejected with 400 VALIDATION_ERROR, and the message says which limit was broken. |
Example
curl -s -X POST "$BASE/conversation/store" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"content": "Not tonight — the stop-order is active until the incident review closes.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"additional_metadata": {
"author": "deploy-assistant"
}
}'const result = await hm("/conversation/store", {
"content": "Not tonight — the stop-order is active until the incident review closes.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"additional_metadata": {
"author": "deploy-assistant"
}
});result = hm("/conversation/store", {
"content": "Not tonight — the stop-order is active until the incident review closes.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"additional_metadata": {
"author": "deploy-assistant"
}
})result := hm("/conversation/store", jsonBody(`{
"content": "Not tonight — the stop-order is active until the incident review closes.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa",
"additional_metadata": {
"author": "deploy-assistant"
}
}`))Response
{
"ok": true,
"data": {
"message": "Conversation exchange stored successfully",
"id": "01a0afb5-e514-780d-bee5-6f85ce258536",
"receipt_id": "01a03163-2e2c-74f8-8a54-1125c38b68dc"
},
"request_id": "…"
}Errors
400 VALIDATION_ERROR for empty content, session id, or exchange pair id; 429; 401. If this call fails after a successful turn, the chat already succeeded — do not surface an error to the user. On a 5xx, or a failure before any response began, retry it out of band with a backoff that ends. A 4xx other than 429 is permanent for that request: the same request will be refused again, so fix it or set it aside, and never let it block the stores queued behind it. A timed-out store may have landed, and the service does not dedupe a re-sent reply half: verify first: replay the session with /conversation/recall and look for the reply under its exchange_pair_id, then re-send only if it is not there.
POST /conversation/recall
Explicit episode recall over conversation history. Two modes: with session_id omitted or null it searches semantically across all sessions, one slot per exchange, and returns each exchange whole: the half that matched, plus its other half (so up to two records per slot); with a session_id it returns that session’s exchanges in full, both halves, newest first by the time each was stored — the replay path. A session made by /conversation/fork replays its own messages, then its parent’s from before the fork. The loop already does cross-session conversation recall internally; this endpoint is for doing it deliberately.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
query | string | yes | The search text. Matched against both sides of every exchange. |
session_id | string or null | no | Null searches across sessions; a value replays that session. |
top_k | integer | no | Result count, default 10, capped by your subscription. |
Example
curl -s -X POST "$BASE/conversation/recall" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"query": "production snapshot rebuild schedule Mondays 03:00 UTC",
"session_id": null,
"top_k": 3
}'const result = await hm("/conversation/recall", {
"query": "production snapshot rebuild schedule Mondays 03:00 UTC",
"session_id": null,
"top_k": 3
});result = hm("/conversation/recall", {
"query": "production snapshot rebuild schedule Mondays 03:00 UTC",
"session_id": None,
"top_k": 3
})result := hm("/conversation/recall", jsonBody(`{
"query": "production snapshot rebuild schedule Mondays 03:00 UTC",
"session_id": null,
"top_k": 3
}`))Response
{
"ok": true,
"data": {
"memories": [
{
"id": "01a0a3db-b5a0-77ed-bc51-147f51059aa4",
"content": "When is the staging database rebuilt?",
"metadata": {
"author": "ops-lead",
"type": "conversation_exchange",
"session_id": "docs-example-6df0986250",
"turn_number": 4,
"role": "user",
"exchange_pair_id": "xp-docs-6df0986250",
"timestamp": 1789780724.6888163,
…
}
},
{
"id": "01a05826-8904-7357-b371-2d2fdbd0fb56",
"content": "The staging database is rebuilt Mondays 03:00 UTC from the previous night's production snapshot.",
"metadata": {
"author": "deploy-assistant",
"type": "conversation_exchange",
"session_id": "docs-example-6df0986250",
"turn_number": 4,
"role": "assistant",
"exchange_pair_id": "xp-docs-6df0986250",
"timestamp": 1789780727.848919,
"semantic_relativity": 0.75301,
"resonance_score": 0.75301,
…
}
}
]
},
"request_id": "01a06ba7-26d0-739c-8c70-9b0900ba555e"
}A real response to a query worded like the reply. The reply is the half that matched, so it carries the match scores; the question was placed before it, so the exchange arrives whole. Metadata is abridged here (…): every record also carries the service’s own bookkeeping fields.
Errors
404 MEMORY_NOT_FOUND when nothing matches — normal on a new workspace, proceed; 400 for an empty query; 429; 401.
POST /conversation/fork
Starts a new session as a fork of an existing one, cut at the moment of the call. The new session’s /turn and /conversation/recall read the parent’s conversation up to the fork, then only what the new session stores. Messages the parent stores later are not the fork’s, and the parent never sees the fork’s. Nothing is copied: one marker record is stored in the new session, and it never comes back from any recall or search. Deleting a parent message removes it from the fork too. A fork can itself be forked. Use it to clone an agent that continues from the same context: run the clone on new_session_id.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
session_id | string | yes | The session to fork. At most 256 characters. |
new_session_id | string | yes | The new session. Must hold no records yet and must differ from session_id. At most 256 characters. |
author | string | no | Who forked it; stored on the marker. At most 256 characters. |
Example
curl -s -X POST "$BASE/conversation/fork" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"session_id": "planner-main",
"new_session_id": "planner-clone-2",
"author": "planner"
}'const result = await hm("/conversation/fork", {
"session_id": "planner-main",
"new_session_id": "planner-clone-2",
"author": "planner"
});result = hm("/conversation/fork", {
"session_id": "planner-main",
"new_session_id": "planner-clone-2",
"author": "planner"
})result := hm("/conversation/fork", jsonBody(`{
"session_id": "planner-main",
"new_session_id": "planner-clone-2",
"author": "planner"
}`))Response
{
"ok": true,
"data": {
"session_id": "planner-clone-2",
"forked_from": "planner-main",
"fork_time": 1790581204.5316944,
"marker_id": "01a1c7e2-4f0b-7a91-9c3d-5e8b21f6a0d4",
"receipt_id": "01a1c7e2-5a17-7c02-8e4f-b3d90a6c1e72"
},
"request_id": "01a1c7e2-3e88-79f5-a2b6-0c4d8f13e957"
}fork_time is the cut, in the service’s epoch seconds: the parent’s messages stored before it are the new session’s history. marker_id is the marker record; receipt_id is the conversation.fork receipt.
Errors
404 MEMORY_NOT_FOUND when session_id holds no conversation; 409 CONFLICT when new_session_id already holds records — pick a new id; 400 VALIDATION_ERROR when either id is empty or both are the same, with details.field naming the field; 429; 401.
POST /create_memory
Deliberate durable memory: decisions and their rationale, lessons learned, durable facts, outcomes. Stored forever, recallable from any session, deletable by its id or by its metadata. Attach consistent metadata keys — they are what make filtered recall and surgical deletion possible later.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
content | string | yes | The fact, stated so it stands alone. Empty content is rejected; at most 200,000 characters. |
metadata | object | no | Your keys and values. tenant_id is overwritten server-side; everything else is yours. tags, project, topic, and author are useful conventions. Metadata is for labels and small facts; long text belongs in content. Limits: at most 64 keys; a key up to 128 characters; a string value up to 8,000 characters; a list value up to 64 items. Anything beyond is rejected with 400 VALIDATION_ERROR, and the message says which limit was broken. |
Example
curl -s -X POST "$BASE/create_memory" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {
"project": "atlas",
"topic": "ops"
}
}'const result = await hm("/create_memory", {
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {
"project": "atlas",
"topic": "ops"
}
});result = hm("/create_memory", {
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {
"project": "atlas",
"topic": "ops"
}
})result := hm("/create_memory", jsonBody(`{
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {
"project": "atlas",
"topic": "ops"
}
}`))Response
{
"ok": true,
"data": {
"message": "Memory created successfully",
"id": "01a05f7c-7b0a-70be-b939-427cd2ea8b63",
"receipt_id": "01a043aa-e42e-7b78-b802-2c8d8d246178"
},
"request_id": "…"
}Errors
400 empty content; 429; 401.
POST /recall_memory
Semantic recall over deliberate memories only — conversation history is a separate record class with its own endpoint. Results are relevance-ranked.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
query | string | yes | The search text. |
top_k | integer | no | Result count, default 5, capped by your subscription. |
Example
curl -s -X POST "$BASE/recall_memory" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"query": "when is it safe to run a staging migration?",
"top_k": 5
}'const result = await hm("/recall_memory", {
"query": "when is it safe to run a staging migration?",
"top_k": 5
});result = hm("/recall_memory", {
"query": "when is it safe to run a staging migration?",
"top_k": 5
})result := hm("/recall_memory", jsonBody(`{
"query": "when is it safe to run a staging migration?",
"top_k": 5
}`))Response
{
"ok": true,
"data": {
"memories": [
{
"id": "01a05f7c-7b0a-70be-b939-427cd2ea8b63",
"content": "The staging database is rebuilt every Monday 03:00 UTC; do not schedule migrations near that window.",
"metadata": {
"project": "atlas",
"topic": "ops",
"tenant_id": "hm_tenant_9f2c1a7b3d4e5f60"
}
}
]
},
"request_id": "…"
}Errors
404 MEMORY_NOT_FOUND when nothing relevant is stored yet — treat it as an empty result, not a failure; 400 empty query; 429; 401.
POST /recall_with_metadata
Semantic recall scoped to exact metadata matches. Every given key must match; fewer keys match more records. This is also how authorship filters: {"author": "scout-agent"} returns only that agent’s records.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
query | string | yes | The search text. |
metadata | object | yes | Exact-match filters, all required to match. An empty object is rejected. The same size limits apply as on stored metadata: 64 keys, 128-character keys, 8,000-character string values, 64-item lists. |
top_k | integer | no | Result count, default 10, capped by your subscription. |
Example
curl -s -X POST "$BASE/recall_with_metadata" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"query": "database maintenance",
"metadata": {
"project": "atlas"
},
"top_k": 10
}'const result = await hm("/recall_with_metadata", {
"query": "database maintenance",
"metadata": {
"project": "atlas"
},
"top_k": 10
});result = hm("/recall_with_metadata", {
"query": "database maintenance",
"metadata": {
"project": "atlas"
},
"top_k": 10
})result := hm("/recall_with_metadata", jsonBody(`{
"query": "database maintenance",
"metadata": {
"project": "atlas"
},
"top_k": 10
}`))Response
{
"ok": true,
"data": {
"memories": [
{
"id": "…",
"content": "…",
"metadata": {
"project": "atlas",
"topic": "ops"
}
}
],
"receipt_id": "…"
},
"request_id": "…"
}Errors
404 when nothing matches; 400 for an empty query or empty metadata; 429; 401.
POST /delete_by_metadata
Soft-deletes every record matching all given metadata keys. Deleted records leave recall immediately and remain recoverable until purged; every deletion is receipted. Your API key is the authorization (included with your subscription) — what your agents are allowed to delete is your harness’s policy to enforce. Mind the breadth of the filter: fewer keys match more records.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
metadata | object | yes | Exact-match filters; everything matching all of them is soft-deleted. The same size limits apply as on stored metadata: 64 keys, 128-character keys, 8,000-character string values, 64-item lists. |
Example
curl -s -X POST "$BASE/delete_by_metadata" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"metadata": {
"project": "atlas",
"topic": "ops"
}
}'const result = await hm("/delete_by_metadata", {
"metadata": {
"project": "atlas",
"topic": "ops"
}
});result = hm("/delete_by_metadata", {
"metadata": {
"project": "atlas",
"topic": "ops"
}
})result := hm("/delete_by_metadata", jsonBody(`{
"metadata": {
"project": "atlas",
"topic": "ops"
}
}`))Response
{
"ok": true,
"data": {
"message": "Memory deletion by metadata completed successfully",
"deleted_records": 3,
"deleted_exchanges": 0,
"deleted_points": 4
},
"request_id": "01a020d4-5a2f-73d6-8409-448f16583231"
}A real response: three notes matched, one of them long enough to be stored as two chunks. The three numbers count the same deletion in three units, and they differ on purpose:
| Field | Counts |
|---|---|
deleted_records | Records, as a recall shows them: one memory is one record however many chunks it is stored as. This is the number to show a person. |
deleted_exchanges | Distinct conversation exchanges among those records. A metadata recall gives an exchange one result — its question and its reply together — so three exchanges are six records. |
deleted_points | Stored rows marked deleted. A long record is several rows, so this is never smaller than deleted_records. It is the unit storage usage and /purge_deleted_memories work in. |
A filter that matches nothing is a 200 with all three counts at zero, not an error — so is repeating a delete that already ran.
Every call writes one memory.delete_by_metadata receipt, readable through GET /receipts, including a call that matched nothing. Its outputs_ref carries deleted_count (stored rows, the same number as deleted_points), deleted_records and deleted_exchanges.
Errors
401 when the plan does not enable destructive operations; 403 when the key's level is below Can delete, which this route needs; 429.
POST /purge_deleted_memories
Permanently removes soft-deleted records older than the given age. Purged records cannot be recovered; until this runs, soft-deleted records are recoverable.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
older_than_days | integer | no | Minimum age of soft-deleted records to purge. Default 30. |
Example
curl -s -X POST "$BASE/purge_deleted_memories" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"older_than_days": 30
}'const result = await hm("/purge_deleted_memories", {
"older_than_days": 30
});result = hm("/purge_deleted_memories", {
"older_than_days": 30
})result := hm("/purge_deleted_memories", jsonBody(`{
"older_than_days": 30
}`))Response
{
"ok": true,
"data": {
"message": "Soft-deleted memories purged successfully",
"deleted_count": 12
},
"request_id": "…"
}Errors
401 when the plan does not enable destructive operations; 403 when the key's level is below Can delete and purge, which this route needs; 429.
POST /purge_memories
Deletes every record the workspace holds — memories and conversation alike. It is immediate and irreversible. Your cluster itself is untouched as infrastructure; its Hivemind records are removed. Plan-gated as a destructive operation and receipted. The response’s deleted_count is how many stored pieces were removed — the same number the memory.purge_tenant receipt records. The workspace’s earlier receipts are removed with the records; that one receipt is written afterwards and is what remains.
Request fields
No request body.
Example
curl -s -X POST "$BASE/purge_memories" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{}'const result = await hm("/purge_memories", {});result = hm("/purge_memories", {})result := hm("/purge_memories", jsonBody(`{}`))Response
{
"ok": true,
"data": {
"message": "Tenant memories have been purged successfully",
"deleted_count": 3
},
"request_id": "01a09cb8-cf73-724a-842b-0af6a50d48fa"
}Errors
401 when the plan does not enable destructive operations; 403 when the key's level is below Can delete and purge, which this route needs; 429.
GET /receipts
The audit trail, newest first. Every operation — store, recall, compile, delete — emits a persisted receipt recording what ran, what it consumed, and what it produced, with lineage back to the request that caused it.
Query parameters
| Field | Type | Required | Meaning |
|---|---|---|---|
limit | integer | no | 1 to 500, default 50. |
session_id | string | no | Only receipts from this session. |
operation | string | no | Only this operation, for example context.compile or conversation.store. |
Example
curl -s "$BASE/receipts?session_id=session-1&limit=50" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmGet("/receipts?session_id=session-1&limit=50");result = hm_get("/receipts?session_id=session-1&limit=50")result := hmGet("/receipts?session_id=session-1&limit=50")Response
{
"ok": true,
"data": {
"receipts": [
{
"receipt_id": "…",
"operation": "context.compile",
"status": "ok",
"created_at": "2026-08-26T01:20:11.412331+00:00",
"tenant_id": "hm_tenant_…",
"inputs_ref": {
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192
},
"outputs_ref": {
"token_count": 3210
}
}
],
"count": 1
},
"request_id": "…"
}GET /memory/{memory_id}
Exact lookup of one record by the id that create_memory returned or that any recall hit carried. Scoped to your workspace: an id from another workspace is a 404, not a leak. A soft-deleted record answers 404 until it is purged. The shape is the same as a recall hit — id, content, metadata exactly as stored, service-stamped fields included.
Path parameters
| Field | Type | Required | Meaning |
|---|---|---|---|
memory_id | string | yes | The record id. 1 to 256 characters. |
Example
curl -s "$BASE/memory/01a0f6ad-acdc-7d3c-a986-cecaf06a6d3c" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmGet("/memory/01a0f6ad-acdc-7d3c-a986-cecaf06a6d3c");result = hm_get("/memory/01a0f6ad-acdc-7d3c-a986-cecaf06a6d3c")result := hmGet("/memory/01a0f6ad-acdc-7d3c-a986-cecaf06a6d3c")Response
{
"ok": true,
"data": {
"memory": {
"id": "01a0f6ad-acdc-7d3c-a986-cecaf06a6d3c",
"content": "Decision: the lookup route returns exactly one record by its id.",
"metadata": {
"content_type": "decision",
"topic": "api",
"tenant_id": "hm_tenant_107eb8ef49751bd9",
"written_by_key": "api_key_011T2CW29YDCFKWD3KHNNS0VMW",
"kind": "memory",
"recall_mode": "semantic",
"storage_mode": "inline",
"content_length_bytes": 64,
"embedding_model": "all-MiniLM-L6-v2",
"timestamp": 1789814850.9363692,
"recall_count": 0,
"memetic_similarity": 1.0,
"semantic_relativity": 1.0,
"gravitational_pull": 1.0000000632769623,
"spacetime_coordinate": 0.9999371247296064
}
}
},
"request_id": "01a011aa-77db-7253-b1b7-e9b68189d8dd"
}An unknown id, another workspace’s id, or a soft-deleted record answers 404 MEMORY_NOT_FOUND with details.field = "memory_id".
DELETE /memory/{memory_id}
Soft-deletes exactly one record, by the id that create_memory returned, that a recall hit carried, or that GET /memory/{id} answered with. It deletes what that GET would fetch and nothing else: a long record stored in chunks goes whole, the other half of a conversation exchange stays, and no other record is touched however similar its metadata. The record leaves recall at once and stays recoverable until purged. Use it when you hold the id; use /delete_by_metadata to remove everything matching a filter. Receipts are your audit trail and cannot be deleted this way.
Path parameters
| Field | Type | Required | Meaning |
|---|---|---|---|
memory_id | string | yes | The record id. 1 to 256 characters. No request body. |
Example
curl -s -X DELETE "$BASE/memory/01a009c2-b0e3-774a-916c-fefb744fd6e1" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmDelete("/memory/01a009c2-b0e3-774a-916c-fefb744fd6e1");result = hm_delete("/memory/01a009c2-b0e3-774a-916c-fefb744fd6e1")result := hmDelete("/memory/01a009c2-b0e3-774a-916c-fefb744fd6e1")Response
{
"ok": true,
"data": {
"message": "Memory deleted",
"id": "01a009c2-b0e3-774a-916c-fefb744fd6e1",
"deleted": true,
"deleted_records": 1,
"deleted_exchanges": 0,
"deleted_points": 1
},
"request_id": "01a0350b-83fc-77a5-a0ec-fad4aaf36fc5"
}An unknown id, another workspace’s id, an id that is not a valid record id, or a record already deleted answers 404 MEMORY_NOT_FOUND — so deleting the same id twice answers 404 the second time. Nothing is written and no receipt is created.
The three counts are the same units /delete_by_metadata reports: deleted_records is always 1, deleted_exchanges is always 0 because the other half of an exchange is never removed, and deleted_points is the stored rows marked — more than 1 for a long record stored in chunks. A deletion that removed something writes one memory.delete receipt, readable through GET /receipts, whose outputs_ref carries deleted: true and points, the same number as deleted_points.
Errors
404 MEMORY_NOT_FOUND as above; 401 for a missing or invalid key, or when your subscription does not enable destructive operations; 403 when the key belongs to another project, or when its level is below Can delete; 429.
GET /receipts/{receipt_id}
Exact lookup of one receipt by the receipt_id that any write, recall, or turn returned. Scoped to your workspace. The shape matches one entry of the receipts listing.
Path parameters
| Field | Type | Required | Meaning |
|---|---|---|---|
receipt_id | string | yes | The receipt id. 1 to 256 characters. |
Example
curl -s "$BASE/receipts/01a06373-1673-78d3-a307-21b8d31b7276" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmGet("/receipts/01a06373-1673-78d3-a307-21b8d31b7276");result = hm_get("/receipts/01a06373-1673-78d3-a307-21b8d31b7276")result := hmGet("/receipts/01a06373-1673-78d3-a307-21b8d31b7276")Response
{
"ok": true,
"data": {
"receipt": {
"receipt_id": "01a06373-1673-78d3-a307-21b8d31b7276",
"operation": "memory.create",
"status": "ok",
"created_at": "2026-09-11T07:30:47.410880+00:00",
"tenant_id": "hm_tenant_107eb8ef49751bd9",
"principal_id": "hm_tenant_107eb8ef49751bd9",
"agent_id": null,
"session_id": null,
"turn_id": null,
"source_event_id": null,
"trace_id": null,
"parent_receipt_id": null,
"request_id": "01a08f85-8645-75cd-a24b-385f380c5db1",
"idempotency_key": "01a08f85-8645-75cd-a24b-385f380c5db1",
"inputs_ref": {
"content_preview": "Decision: the lookup route returns exactly one record by its id.",
"metadata": {
"content_type": "decision",
"tenant_id": "hm_tenant_107eb8ef49751bd9",
"topic": "api"
}
},
"outputs_ref": {
"memory_id": "01a050eb-fe23-778c-8664-394df14040e0"
},
"metadata": {},
"source_receipt_ids": []
}
},
"request_id": "01a0d86e-fd5f-7f15-a42d-3a84b275e985"
}Not found
Unknown id or another workspace's receipt: HTTP 404 with MEMORY_NOT_FOUND and details.field = receipt_id.
GET /usage
The authenticated workspace’s own usage: its record counts, and the limits your subscription carries (the wire field is named plan). Storage is your own cluster and carries no allowance from us — this reports what is there, and nothing is refused on it. It is not your bill: billing is metered on compiled tokens. It also reports the calling key, what that key may remove, and which projects it can reach. The key’s part answers even before any cluster is connected: storage.error is then "no_cluster" and counts is null. Like every route it needs X-Hivemind-Project, so a client needs one project id before its first call; once it has one, projects lists the rest the key can reach. If your cluster is unreachable the response reports storage.error = "cluster_unreachable" rather than failing.
Example
curl -s "$BASE/usage" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmGet("/usage");result = hm_get("/usage")result := hmGet("/usage")Response
storage reports the mode. plan.rate_limit of 0 means no requests-per-minute limit. key is the calling key’s own settings: its id, its delete level in destructive ("none", "delete" or "purge"), and projects, the projects it reaches, or null when it reaches every project. destructive is never null here: a key with no level set is refused as invalid, 401, before it reaches this route. plan.allow_destructive is the workspace’s own switch, which applies on top of the key’s level. destructive_access is the service’s verdict after applying both — read destructive_access.delete and destructive_access.purge rather than working it out from the other fields. projects lists every project the key can reach, each with its id, name, whether a cluster is connected, and whether it is the workspace default; a key with a list of projects lists those that still exist and no others, as in the example below. A project’s cluster address and credential are never included. Reading this writes nothing: no receipt, and no refusal. counts.memories counts every memory point, conversation included; counts.receipts counts the audit trail.
{
"ok": true,
"data": {
"plan": {
"name": "hive",
"max_top_k": 50,
"rate_limit": 0,
"allow_destructive": true,
"receipts_enabled": true
},
"storage": {
"mode": "byo"
},
"counts": {
"memories": 1840,
"receipts": 2615
},
"tenant_id": "hm_tenant_107eb8ef49751bd9",
"key": {
"id": "api_key_01SETB4MGGY5WKSZE2MC4X8R0Y",
"destructive": "none",
"projects": [
"proj_30e37f2dee65"
]
},
"destructive_access": {
"delete": false,
"purge": false
},
"projects": [
{
"id": "proj_30e37f2dee65",
"name": "Research",
"connected": true,
"default": false
}
]
},
"request_id": "01a03f00-6ad4-7422-8780-4f19c58d2e90"
}GET /export
Paginated export of everything the workspace owns: memories and conversation, then receipts. Content and metadata; vectors are not exported. Follow next_cursor until done is true. This is the portability contract — your data leaves whenever you ask.
Query parameters
| Field | Type | Required | Meaning |
|---|---|---|---|
cursor | string | no | The next_cursor from the previous page. Omit for the first page. |
limit | integer | no | 1 to 500 records per page, default 200. |
Example
curl -s "$BASE/export?limit=200" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT"const result = await hmGet("/export?limit=200");result = hm_get("/export?limit=200")result := hmGet("/export?limit=200")Response
The first page also writes a data.export receipt into your audit trail, so every export is itself on the record.
{
"ok": true,
"data": {
"records": [
{
"id": "…",
"content": "…",
"metadata": {}
}
],
"count": 200,
"phase": "memories",
"next_cursor": "bWVtb3JpZXM6MjAw",
"done": false
},
"request_id": "…"
}POST /context/compile
The compiler alone: no store, no recall. For custom loops that run their own recall (or none) and want the packet built from exactly what they pass. /turn is this endpoint composed with the store and both recall searches; everything said about the packet there applies here.
Request fields
| Field | Type | Required | Meaning |
|---|---|---|---|
session_id | string | yes | Conversation identifier. |
messages | array of objects | yes | The history to compile, oldest first, real timestamps — same two shapes as /turn's messages (chat form incl. role: "tool" with metadata.tool_call_id/tool_name, or interface events incl. tool.call.finished; one shape per request, never mixed). |
turn_number | integer | yes | Monotonic per session. |
budget_total | integer | yes | Packet budget; maximum 128,000. |
user_input | string | no | The current message, when not already in messages. |
operator_briefing | string | no | As on /turn. |
holds | array of objects | no | As on /turn. |
artifacts_context | string | no | As on /turn. |
recalled_conversation | array of objects | no | Your own recall results to include, in the shape /conversation/recall returns. |
recalled_memories | array of objects | no | Your own memory recall results, in the shape /recall_memory returns. |
policy | object | no | Compiler policy overrides, applied as an overlay: omitted fields keep their budget-derived values. |
config | object | no | Advanced compiler configuration. |
event_id | string | no | Your own event identifier, echoed in the trace. |
trace_id | string | no | Your own trace identifier, echoed in the trace. |
timestamp | number | no | Unix time of the compile, when you need to pin it. |
Example
curl -s -X POST "$BASE/context/compile" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"session_id": "session-1",
"messages": [
{
"role": "user",
"content": "Summarise where we are.",
"timestamp": 1787600300.0
}
],
"turn_number": 8,
"budget_total": 4096
}'const result = await hm("/context/compile", {
"session_id": "session-1",
"messages": [
{
"role": "user",
"content": "Summarise where we are.",
"timestamp": 1787600300.0
}
],
"turn_number": 8,
"budget_total": 4096
});result = hm("/context/compile", {
"session_id": "session-1",
"messages": [
{
"role": "user",
"content": "Summarise where we are.",
"timestamp": 1787600300.0
}
],
"turn_number": 8,
"budget_total": 4096
})result := hm("/context/compile", jsonBody(`{
"session_id": "session-1",
"messages": [
{
"role": "user",
"content": "Summarise where we are.",
"timestamp": 1787600300.0
}
],
"turn_number": 8,
"budget_total": 4096
}`))Response
{
"ok": true,
"data": {
"bundle": {
"messages": [
"…"
],
"token_count": 78,
"source_counts": { "conversation": 1, "system": 1 },
"budget_total": 4096,
"budget_available": 4018,
"conversation_tokens": 31,
"hold_tokens": 0,
"trace": { "session_id": "session-1", "turn_number": 8,
"budget_used": 78, "budget_available": 4018 }
},
"receipt": {
"operation": "context.compile"
}
},
"request_id": "…"
}Errors
400 for an empty session id or a budget above 128,000; 429; 401.