Documentation
Hivemind gives your agents persistent shared memory and a working-context compiler: store what matters, recall it semantically, and assemble a token-budgeted, fully audited prompt for every turn. Your model, your key — Hivemind never calls an LLM.
Quickstart
1. Get your key
- Sign in and subscribe (14-day trial, card up front, nothing billed today).
- Under Projects, open your project and connect your Qdrant cluster — its URL and API key. Your memory lives there, not with us. Any reachable Qdrant works: self-host the open-source server on your own infrastructure, or use Qdrant Cloud — its free tier is enough to start.
- In the same panel, press New key, name it, and choose what it reaches — All projects, or a role, which reaches the projects that list it — and what it may delete. A new key can read and write but not delete until you allow it. It is shown once — store it as
HIVEMIND_API_KEY. - Copy the id of the project to work in, shown beside the project's name (
proj_…), and store it asHIVEMIND_PROJECT.
2. Integrate — any language
Hivemind is a plain HTTPS API: the whole loop is two JSON calls per exchange, from any language. The wire protocol below is the complete integration contract, with the exchange translated into curl, JavaScript, Python, and Go.
3. Prove the connection
A new workspace starts empty — nothing enters your cluster except what you and your agents store, the collections the service creates on first request, and the operation receipts it keeps there for you. Prove the connection with one store and one recall:
curl -s https://api.hivemind.militant.ai/hivemind/v1/create_memory \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{"content": "Hivemind connected: this workspace remembers from here on.", "metadata": {"topic": "setup"}}'
curl -s https://api.hivemind.militant.ai/hivemind/v1/recall_memory \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{"query": "when did this workspace start remembering?", "top_k": 5}'
Point your agents at this documentation for how the system works — the loop, what to store, and the full reference are written to be read by them as much as by you.
The loop: three lines inside your agent
packet = POST /turn { user_input, session, budget, pair-id } # store → recall → compile
reply = your model, run on packet.bundle.messages # your model, your key
POST /conversation/store { reply, same pair-id } # completes the exchange
Working code for that loop, in four languages, is in the wire protocol.
One /turn is one request: the service stores the user
message (receipted), semantically recalls relevant memories and past
conversation, reads back the session’s recent conversation from
what it stored, and compiles it with the recalled records and active holds
into a token-budgeted bundle. Your host sends only the new message. The response’s bundle.messages is a
contiguous, internally-ordered block: use it as the entire
prompt, or surround it — your system prompt above, turn-local instructions
below; the packet keeps its place either way. Never insert into or reorder
inside the block.
The store call files the reply and completes the exchange: the whole
loop is two calls, with your model between them.
Conversation is remembered as call/response exchanges. Recall matches your query against both sides of every past exchange and returns whole exchanges — one result slot per exchange, rendered call-then-response, never an answer without the message that produced it. Facts that appear only in a reply are just as findable as the calls that prompted them. Durable facts that should stand alone — decisions, outcomes, lessons — belong in deliberate memory.
The wire protocol — any language
The service is language-agnostic. One exchange is two HTTPS calls, with your model between them; any client library is a convenience layer over exactly this.
Call 1 (/turn) stores the user message, recalls, and
returns the compiled prompt packet — the response's
data.bundle.messages is the prompt block for your model,
data.receipt the audit record. Call 2
(/conversation/store) records the reply, completing the
exchange with the same exchange_pair_id and
turn_number. The whole exchange, translated:
# 1. compile the turn
curl -s https://api.hivemind.militant.ai/hivemind/v1/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"
}'
# ... run your model on data.bundle.messages ...
# 2. record the reply (same exchange_pair_id + turn_number)
curl -s https://api.hivemind.militant.ai/hivemind/v1/conversation/store \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: $HIVEMIND_PROJECT" \
-H "Content-Type: application/json" \
-d '{
"content": "Yes — window is clear until 03:00 UTC.",
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa"
}'
const 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",
};
const pairId = "xp-" + uuidv7().replace(/-/g, ""); // fresh UUIDv7 per turn (any uuidv7 lib)
// 1. compile the turn
const turn = await fetch(`${BASE}/turn`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
user_input: userInput,
session_id: "session-1",
turn_number: 7,
budget_total: 8192,
exchange_pair_id: pairId,
}),
}).then(r => r.json());
const reply = await callYourLlm(turn.data.bundle.messages);
// 2. record the reply (same exchange_pair_id + turn_number)
await fetch(`${BASE}/conversation/store`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({
content: reply,
session_id: "session-1",
turn_number: 7,
role: "assistant",
exchange_pair_id: pairId,
}),
});
import os, requests # any HTTP client works the same way; mint UUIDv7 ids
BASE = "https://api.hivemind.militant.ai/hivemind/v1"
HEADERS = {
"Authorization": f"Bearer {os.environ['HIVEMIND_API_KEY']}",
"X-Hivemind-Project": os.environ["HIVEMIND_PROJECT"],
}
pair_id = "xp-" + uuid_v7_hex() # fresh UUIDv7 per turn (uuid6 lib, or uuid.uuid7 on 3.14+)
# 1. compile the turn
turn = requests.post(f"{BASE}/turn", headers=HEADERS, json={
"user_input": user_input,
"session_id": "session-1",
"turn_number": 7,
"budget_total": 8192,
"exchange_pair_id": pair_id,
}).json()
reply = call_your_llm(turn["data"]["bundle"]["messages"])
# 2. record the reply (same exchange_pair_id + turn_number)
requests.post(f"{BASE}/conversation/store", headers=HEADERS, json={
"content": reply,
"session_id": "session-1",
"turn_number": 7,
"role": "assistant",
"exchange_pair_id": pair_id,
})
base := "https://api.hivemind.militant.ai/hivemind/v1"
pairID := "xp-" + uuidv7Hex() // fresh UUIDv7 per turn (github.com/google/uuid: uuid.NewV7())
post := func(path string, payload map[string]any) map[string]any {
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", base+path, bytes.NewReader(body))
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)
return out
}
// 1. compile the turn
turn := post("/turn", map[string]any{
"user_input": userInput, "session_id": "session-1",
"turn_number": 7, "budget_total": 8192,
"exchange_pair_id": pairID,
})
reply := callYourLLM(turn["data"].(map[string]any)["bundle"].(map[string]any)["messages"])
// 2. record the reply (same exchange_pair_id + turn_number)
post("/conversation/store", map[string]any{
"content": reply, "session_id": "session-1",
"turn_number": 7, "role": "assistant",
"exchange_pair_id": pairID,
})
The protocol's rules — every client must honour these:
- Pairing. Generate a fresh
exchange_pair_idper turn; the record call must reuse it and the turn number, so the exchange is stored as one call/response pair. Any collision-resistant unique string works — the project's convention is a time-ordered UUIDv7. - Aborted generation: record nothing. If the user cancels mid-stream, make no call — the stored user message stands as an unanswered call, and the next message is a new turn. Never record a partial reply as if it was said, and never send empty content.
- Never retry a timed-out turn. A timeout does not mean the request failed — re-sending can store the message twice. Retry only requests that failed before any response began.
- Honour
Retry-After. A429carries it; back off that many seconds and retry. That is the service asking your client to slow down, not an error. - Budget ceiling.
budget_totalat most 128,000; a400names the limit. - Name the project. Every request carries
X-Hivemind-Project; without it the request is refused with400.
Every other endpoint speaks the same JSON envelope with the same auth header. The full API reference documents every endpoint — request fields, response shapes, errors — with working examples in all four languages, plus the complete client implementation guide.
The working context — representative output
This is what one /turn (or /context/compile) hands back,
rendered: the trace, the admitted items, the accounting, and the compiled
messages your model receives. Toggle the response JSON to see the exact
wire shape.
TRACE
- Session ID
- session_001
- Turn Number
- 14
- Built At
- 2026-08-30T12:11:37Z
- Event ID
- event_001
- Trace ID
- trace_001
- User Input
- Find the lowest-risk path to stabilise checkout latency before peak traffic.
- Context Messages
- 19
ITEMS (KIND · SOURCE, 5 OF 19)
-
DIRECTIVE · DIRECTIVEdirective-0
Prefer reversible mitigations and preserve checkout stability during active traffic.
-
MEMORY · HIVEMIND9d41c2f0…
Checkout latency spiked during the 2026-06-21 payment provider failover; regional failback plus cache warmup restored p95 within 11 minutes.
-
MESSAGE · RECALLED_CONVERSATIONxp-310a
No - the provider failover in June showed peak-hours changes cost us 11 minutes of degraded p95. Ship in the low-traffic window.
-
HOLD · SYSTEMhold-8c2f
Checkout change freeze: avoid changes that increase regression risk during active checkout traffic.
-
MESSAGE · CONVERSATIONactive input
Find the lowest-risk path to stabilise checkout latency before peak traffic.
+ the remaining 14 messages of this bundle, section markers included
ACCOUNTING (API DATA)
-
POLICYbalanced
Balanced admission inside
max_context_tokenswith per-class caps on memory, tool, and hold items. -
COUNTSsources
Admitted items counted by source: conversation=5, directive=1, hivemind=4, recalled_conversation=2, system=3. Section markers ride along uncounted.
-
BUDGETeconomy
token_count481,budget_available3615,conversation_tokens133,hold_tokens66 — reported on every compile. -
TRACEsnapshot
Compiler diagnostics remain inspectable alongside the assembled bundle.
MESSAGES (COMPILED 19)
-
SYSTEMsection marker
=== AGENT FIXED CONTEXT BUFFER ===
-
SYSTEMdirective
Prefer reversible mitigations and preserve checkout stability during active traffic.
-
SYSTEMstatus line
Context: 433/4096 tokens | 1 holds active (66 tokens) | conversation: 133 tokens | sources: conversation=5, directive=1, hivemind=4, recalled_conversation=2, system=2 | available: 3663 tokens
+ the remaining 16 messages: artifacts, hold, both recall buffers, and the active conversation
{
"ok": true,
"data": {
"bundle": {
"messages": [
{
"id": "context-section-fixed-14",
"role": "system",
"content": "=== AGENT FIXED CONTEXT BUFFER ===",
"tool_call_id": null,
"name": null
},
{
"id": "context-briefing-14",
"role": "system",
"content": "Prefer reversible mitigations and preserve checkout stability during active traffic.",
"tool_call_id": null,
"name": null
},
{
"id": "context-status-14",
"role": "system",
"content": "Context: 433/4096 tokens | 1 holds active (66 tokens) | conversation: 133 tokens | sources: conversation=5, directive=1, hivemind=4, recalled_conversation=2, system=2 | available: 3663 tokens",
"tool_call_id": null,
"name": null
},
{
"id": "context-artifacts-14",
"role": "system",
"content": "Available files:\nCheckout Latency Playbook v3.2; SLO dashboard shows checkout p95 above threshold in us-east-1.",
"tool_call_id": null,
"name": null
},
{
"id": "hold-8c2f",
"role": "system",
"content": "=== HOLD ===\nid: hold-8c2f\ntitle: Checkout change freeze\ntokens: 0\nheld_since: turn 0\n---\nCheckout change freeze: avoid changes that increase regression risk during active checkout traffic.\n=== END HOLD (hold-8c2f) | To release: context.release(\"hold-8c2f\") ===",
"tool_call_id": null,
"name": null
},
{
"id": "context-section-semantic-14",
"role": "system",
"content": "=== SEMANTIC RECALL BUFFER ===",
"tool_call_id": null,
"name": null
},
{
"id": "019ea27f-95f8-73a1-8c41-b7e2d09f6a53",
"role": "system",
"content": "Time: 2026-06-07T14:32:11+00:00\nCheckout Latency Playbook summary: prefer traffic shaping, replica scaling, and cache warmup before any config change.",
"tool_call_id": null,
"name": null
},
{
"id": "019eea98-9df8-7c0e-9a2f-47b31d8e5c06",
"role": "system",
"content": "Time: 2026-06-21T14:32:11+00:00\nCheckout latency spiked during the 2026-06-21 payment provider failover; regional failback plus cache warmup restored p95 within 11 minutes.",
"tool_call_id": null,
"name": null
},
{
"id": "019f0ea5-21f8-75d7-b1e0-3a9f4c62b817",
"role": "system",
"content": "Time: 2026-06-28T14:32:11+00:00\nCDN config rollout on 2026-06-28 correlated with elevated checkout errors; rollback was clean and reversible.",
"tool_call_id": null,
"name": null
},
{
"id": "019eeaa1-c5b8-7e24-a6f8-c01b5d3972ea",
"role": "system",
"content": "Time: 2026-06-21T14:42:11+00:00\nScaling the payment worker pool from 8 to 14 replicas resolved queue backpressure without config changes.",
"tool_call_id": null,
"name": null
},
{
"id": "context-section-search-14",
"role": "system",
"content": "=== SEARCH RECALL BUFFER ===",
"tool_call_id": null,
"name": null
},
{
"id": "019eee9b-b778-781c-bd5e-7f9a0b2c4d6e",
"role": "user",
"content": "Can we ship the payment provider migration during business hours?",
"tool_call_id": null,
"name": null
},
{
"id": "019eee9b-cee8-792d-8e6f-8a0b1c3d5e7f",
"role": "assistant",
"content": "No - the provider failover in June showed peak-hours changes cost us 11 minutes of degraded p95. Ship in the low-traffic window.",
"tool_call_id": null,
"name": null
},
{
"id": "context-section-conversation-14",
"role": "system",
"content": "=== ACTIVE CONVERSATION BUFFER ===",
"tool_call_id": null,
"name": null
},
{
"id": "message-0",
"role": "user",
"content": "Checkout latency is spiking again — what did we do last time?",
"tool_call_id": null,
"name": null
},
{
"id": "message-1",
"role": "assistant",
"content": "Last incident we deferred config changes and scaled the payment worker pool first.",
"tool_call_id": null,
"name": null
},
{
"id": "message-2",
"role": "user",
"content": "That worked - latency recovered inside the traffic window.",
"tool_call_id": null,
"name": null
},
{
"id": "message-3",
"role": "assistant",
"content": "Monitoring is live. Ready for the next directive.",
"tool_call_id": null,
"name": null
},
{
"id": "message-4",
"role": "user",
"content": "Find the lowest-risk path to stabilise checkout latency before peak traffic.",
"tool_call_id": null,
"name": null
}
],
"policy": {
"max_context_tokens": 4096,
"max_items": 50,
"max_memory_items": 10,
"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": 481,
"source_counts": {
"conversation": 5,
"directive": 1,
"hivemind": 4,
"recalled_conversation": 2,
"system": 3
},
"budget_total": 4096,
"budget_available": 3615,
"conversation_tokens": 133,
"hold_tokens": 66,
"trace": {
"session_id": "session_001",
"turn_number": 14,
"built_at": "2026-08-30T12:11:37.200426+00:00",
"event_id": "event_001",
"trace_id": "trace_001",
"user_input": "Find the lowest-risk path to stabilise checkout latency before peak traffic.",
"budget_total": 4096,
"budget_used": 481,
"budget_available": 3615,
"conversation_tokens": 133,
"hold_tokens": 66,
"context_message_count": 19,
"source_counts": {
"conversation": 5,
"directive": 1,
"hivemind": 4,
"recalled_conversation": 2,
"system": 3
},
"recalled_conversation_count": 2,
"recalled_memory_count": 4,
"recalled_memory_input_count": 4,
"receipt_id": "e9b3c1a0-59f2-711e-8d2e-0242ac120002",
"parent_receipt_id": null
}
},
"receipt": {
"receipt_id": "e9b3c1a0-59f2-711e-8d2e-0242ac120002",
"operation": "context.compile",
"status": "ok",
"created_at": "2026-08-30T12:11:37.200426+00:00",
"tenant_id": "hm_tenant_9c4e1b22a7d301f6",
"principal_id": "hm_tenant_9c4e1b22a7d301f6",
"session_id": "session_001",
"turn_id": "14",
"source_event_id": "event_001",
"trace_id": "trace_001",
"parent_receipt_id": null,
"source_receipt_ids": [],
"request_id": "f21d7b6e-59f2-711e-8d2e-0242ac120002",
"idempotency_key": "f21d7b6e-59f2-711e-8d2e-0242ac120002",
"inputs_ref": {
"session_id": "session_001",
"turn_number": 14,
"budget_total": 4096,
"recalled_conversation_count": 2,
"recalled_memory_count": 4
},
"outputs_ref": {
"token_count": 481,
"item_ids": [
"context-section-fixed-14",
"context-briefing-14",
"context-status-14",
"context-artifacts-14",
"hold-8c2f",
"context-section-semantic-14",
"019ea27f-95f8-73a1-8c41-b7e2d09f6a53",
"019eea98-9df8-7c0e-9a2f-47b31d8e5c06",
"019f0ea5-21f8-75d7-b1e0-3a9f4c62b817",
"019eeaa1-c5b8-7e24-a6f8-c01b5d3972ea",
"context-section-search-14",
"019eee9b-b778-781c-bd5e-7f9a0b2c4d6e",
"019eee9b-cee8-792d-8e6f-8a0b1c3d5e7f",
"context-section-conversation-14",
"message-0",
"message-1",
"message-2",
"message-3",
"message-4"
],
"source_counts": {
"conversation": 5,
"directive": 1,
"hivemind": 4,
"recalled_conversation": 2,
"system": 3
}
},
"metadata": {
"trace_id": "trace_001",
"event_id": "event_001"
}
}
},
"request_id": "f21d7b6e-59f2-711e-8d2e-0242ac120002"
}
Deliberate memory
# store a durable fact with your metadata
curl -s .../hivemind/v1/create_memory -H "$AUTH" -H "$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"}
}'
# recall it, semantically or filtered
curl -s .../hivemind/v1/recall_memory -H "$AUTH" -H "$JSON" \
-d '{"query": "when is it safe to run a staging migration?", "top_k": 5}'
curl -s .../hivemind/v1/recall_with_metadata -H "$AUTH" -H "$JSON" \
-d '{"query": "database", "metadata": {"project": "atlas"}, "top_k": 10}'
Store decisions and their rationale, lessons learned, durable facts,
and outcomes. Attach consistent metadata keys — they are what make
filtered recall and surgical, audited deletion possible later.
/recall_memory searches
deliberate memories only: conversation history is a
separate record class, recalled automatically inside the loop (or
explicitly via /conversation/recall).
The audit trail
curl -s ".../hivemind/v1/receipts?session_id=session-1&limit=50" -H "$AUTH"
Every operation — store, recall, compile, delete — emits a receipt:
what ran, what it consumed (inputs_ref), and what it
produced (outputs_ref), persisted to your cluster and
queryable by session, turn, or operation. parent_receipt_id
is on every receipt and reads null.
The context window
The compiled packet is not an opaque blob — it has a fixed anatomy, and knowing it is how you place every kind of content deliberately. A real compile looks like this (roles on the left):
system === AGENT FIXED CONTEXT BUFFER ===
system [your operator_briefing — persona, rules, mode flags]
system Context: 3210/8192 tokens | 2 holds active (350 tokens) | ...
system === HOLD === id: stop-order · title · tokens · held_since ...
system [artifacts_context — "Available files:" when supplied]
system === SEMANTIC RECALL BUFFER ===
system [recalled deliberate memories, relevance-ranked]
system === SEARCH RECALL BUFFER ===
user [recalled exchange: the call]
assistant [recalled exchange: the response]
system === ACTIVE CONVERSATION BUFFER ===
user [recent conversation, reading order]
assistant ...
user [the current message — always last]
Two things to read off that structure:
- The fixed context buffer is the boundary your integration works within.
Everything a developer would normally wrap around a prompt has a
named slot inside the packet instead — and content inside is
budget-managed, admission-decided, counted in the status line, and
receipted; content wrapped outside is invisible to all accounting.
External content Its slot System prompt / persona / rules operator_briefing— protected system directive, re-sent per turn, never storedUser profiling, standing operational state holds — re-sent by you each turn until dropped; labeled, token-visible Documents, file excerpts, uploads artifacts_context— this compile onlyMode flags, turn-local steering operator_briefingper-turn overrideTool definitions not context — host configuration; never sent, never stored (see the chat-host section for how tool results enter) - The packet is self-labeling and position-independent. Its section markers announce its regions; nothing inside references absolute position. Stack your own messages above or below and every internal relationship holds — Hivemind can be the entire window or a region within one.
API surface
Every call. All responses arrive as
{"ok": true, "data": ..., "request_id": ...} envelopes; the
envelope carries them.
| Endpoint | What it does |
|---|---|
POST /turn | The composed exchange: store the call, inline recall (both searches), compile |
POST /conversation/store | The reply half of the exchange, paired by exchange_pair_id |
POST /create_memory | Deliberate durable memory with your metadata |
POST /recall_memory | Semantic recall over deliberate memories only |
POST /recall_with_metadata | Semantic recall scoped by exact metadata matches |
POST /conversation/recall | Manual episode recall: session_id=None searches all sessions (one slot per exchange, each returned whole: the half that matched plus its other half); a session id replays that session (both halves, newest-first, up to top_k) |
POST /conversation/fork | Starts a new session as a fork of an existing one, cut now: the new session's turns and replay carry the parent's conversation up to the fork, then its own. See Forking a conversation |
POST /context/compile | Raw compile for custom loops built around your own recall |
POST /purge_deleted_memories | Permanently removes soft-deleted records older than older_than_days (default 30) — deletion becomes final here, not at delete time |
POST /purge_memories | Hard-deletes all memory records immediately — the destructive full reset; always receipted |
GET /receipts | Audit trail, newest-first; filter by session_id / operation |
GET /memory/{id} | Exact lookup of one record by id, workspace-scoped; 404 when unknown, foreign, or soft-deleted |
GET /receipts/{id} | Exact lookup of one receipt by receipt_id, workspace-scoped |
DELETE /memory/{id} | Soft-deletes exactly one record by id — what GET /memory/{id} would fetch: a long record goes whole, the other half of an exchange stays, receipts are never deletable. 404 when unknown, foreign, or already deleted. Out of recall immediately, recoverable until purged, receipted |
GET /usage | Storage, point counts, and the limits your subscription carries |
GET /export | Every record you own, paged. Portability by contract |
POST /delete_by_metadata | Soft-deletes everything matching all given keys. Your key is the authorization (included with your subscription). Out of recall immediately, recoverable until purged, always receipted. The response says what was removed — deleted_records, deleted_exchanges and deleted_points (stored rows) — and a filter that matches nothing answers with zeros. Mind filter breadth — what your agents may delete is your harness's policy |
The turn, in detail
Everything turn() accepts, and what each input does:
| Parameter | Behavior |
|---|---|
user_input | The initiating message. Stored as the call half of a new exchange, used as the query for both recall searches, and always the packet's last message. Must be non-empty |
budget_total | The compile's token budget — the single number that governs the whole window. Set per session or per turn; if you wrap the packet, size it as your model window minus your wrapper. Maximum 128,000 — a larger value is rejected with 400 VALIDATION_ERROR whose details.max_budget_total names the limit. The budget bounds what is surfaced per turn, not what is remembered: memory is unbounded |
messages | Optional. Only what the service does not store and later compiles should see: tool results, as {role, content, timestamp} dicts with real unix timestamps, or as interface events (see Tool use in Building a chat host). Never the conversation itself: the service reads the session's recent conversation back from what it stored, and adds user_input as the last message |
operator_briefing | Your system layer — compiled as a protected directive at the top of the packet, re-sent every turn, never stored, never recalled |
artifacts_context | One-compile content (document excerpts, file listings). Rendered in the fixed buffer this turn only; never stored |
holds | Pinned state riding every compile until you clear it — see the chat-host section for the lifetime contract |
author | Who produced this call — identity metadata, stored on the record and surfaced as the message's name |
recall_top_k | Omit for a budget-scaled candidate pool (the default and usually right); set to force a fixed pool, gated by your subscription's ceiling |
policy | Advanced: a compile-policy override (item caps, include switches) for this turn — most integrations never need it |
And everything it returns:
| Field | Contents |
|---|---|
bundle.messages | The compiled packet — the block you send to your model. Each message is id, role, content, tool_call_id and name. id is the record's id for everything stored — your conversation, recalled exchanges, memories, and the message this turn stored — a uuid7, which carries the time the record was stored; what you sent keeps the ids you gave it; the service's own text carries its labels |
bundle.token_count / budget_total | The packet's measured size against the budget — the context meter |
receipt | The compile's audit record; receipt_id links it in the receipts trail |
stored | The stored call half's {id, receipt_id} — an empty object if the store degraded (see warnings) |
recalled_memories / recalled_conversation | How many records each recall search contributed to the compile |
session_history | How many stored messages of this session were read back as the recent conversation: newest first, as many as budget_total could hold |
warnings | Degradations that didn't fail the turn — a failed store or recall search reports here and the compile proceeds without it |
timings | Server-side phase breakdown, seconds: embed_s (preparing your input for the recall searches), store_and_recall_s (storing your message, both recall searches and reading back the session's recent conversation), completion_s (fetching the other half of recalled exchanges), compile_s (building the packet), receipts_s (writing the turn's receipts), total_s (the whole call). A slow turn names its own bottleneck |
The client-side state these calls assume (the open exchange and holds) is specified in Building a chat host.
Conventional environment names
Nothing requires these, but examples and tooling use them:
| Env var | Meaning |
|---|---|
HIVEMIND_BASE_URL | Service root — the host only (https://api.hivemind.militant.ai); clients append /hivemind/v1 |
HIVEMIND_API_KEY | Sent as Authorization: Bearer <key>; identifies and tenant-binds in one step |
HIVEMIND_PROJECT | Required: the project to work in, sent as X-Hivemind-Project on every request |
HIVEMIND_TIMEOUT | Client request timeout in seconds — compiles at large budgets are multi-second operations; 120 is a reasonable default |
HIVEMIND_BUDGET_TOTAL | Default compile budget when your host does not set one per session |
Memory & metadata
Every stored record carries a record class, stamped at write time, and the recall paths are class-scoped — with one deliberate exception:
| Class | What | Recalled by |
|---|---|---|
semantic | Deliberate memories (/create_memory) | /recall_memory and the loop's semantic recall — relevance-ranked, this class only. /recall_with_metadata is the exception: it searches every embedded record matching your filter, semantic and temporal alike — scope it with recall_mode when you want one class |
temporal | Conversation exchanges | The loop's conversation recall and /conversation/recall |
diagnostic | Receipts, document references | Never semantically recalled; scrolled by explicit query only |
Metadata is the control surface. Two kinds of keys live
on every record. Service-stamped keys are on every record at
write time — some copied from your request fields
(session_id, turn_number, role,
exchange_pair_id), the rest generated by the service. Filter
on them freely, but don't overwrite them:
| Key | Meaning | Why you care |
|---|---|---|
type | What kind of record this is (conversation_exchange, hivemind_receipt, your own values via metadata) | The broadest filter; conversation records are identified by it |
session_id / turn_number | Which conversation, which turn | Session replay, receipts filtering, chronological ordering of recalled exchanges |
role | Positional: which side of the exchange — user initiated (the call), assistant answered — not who was talking | Drives exchange pairing and rendering; identity lives in author, never here |
exchange_pair_id | The identity of one exchange — both halves share it, nothing else ever does | Drives one-slot-per-exchange dedupe and pair completion in recall |
recall_mode | The record class: semantic, temporal, or diagnostic | Decides which recall path can ever return this record |
storage_mode | inline or chunked; document references carry referential | Long content comes back whole on recall |
timestamp | Unix time at storage | Orders conversation into reading order; drives newest-first returns |
recall_count | How many times recall has returned this record — incremented automatically after every recall | |
semantic_relativity, resonance_score | Query-time relevance: the engine's similarity for this query, and the final ranking score | Read them off recall results to understand why something ranked where it did |
deleted_at | Soft-deletion stamp | Present = out of all recall, recoverable until purged |
Caller-set keys are yours, supplied via
metadata on /create_memory or
additional_metadata on stores. Every one becomes an
exact-match filter in /recall_with_metadata and a deletion
selector in /delete_by_metadata — consistent keys are what
make surgical operations possible later. Two semantics to respect: service-stamped keys in your metadata are
dropped on write (the record is re-stamped), and type may not
use the service's own record types (hivemind_receipt,
conversation_exchange — a 400 names the field); and
list-valued keys match any element, not the exact list —
{"tags": ["contract"]} selects every record carrying that
tag, which widens a deletion filter accordingly. The ones with built-in behavior:
| Key | Built-in behavior |
|---|---|
author | Who produced the message — surfaces as the chat message's name field in compiles and recalls; pass author on /turn and additional_metadata.author on the store |
written_by_key | Set by the service on every record: the id (never the secret) of the API key that wrote it. You cannot set or change it — a value you send is discarded — so unlike author, which is whatever the caller says, it is a fact about who wrote what. It filters like any key, so /recall_with_metadata and /delete_by_metadata can target one key's records |
tags | A list; tags filter like any key |
document_id | Ties records to a document; it scopes /recall_with_metadata for per-document recall |
| anything else | No built-in behavior — pure filter/delete selector. project, customer_id, agent_run: whatever your operations need to slice by |
Long content stays reachable at any depth; recall returns the whole record.
# write with your own keys — every key is a future filter and delete selector
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": "Acme renewal decided: 12 months, $48k, signed 2026-08-12.",
"metadata": {"project": "acme", "tags": ["contract", "renewal"],
"document_id": "acme-msa-2026", "author": "ops-agent-2"}
}'
# later — recall scoped by those keys...
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": "renewal terms", "metadata": {"document_id": "acme-msa-2026"}, "top_k": 5}'
# ...and surgical deletion by the same keys
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": "acme", "tags": ["contract"]}}'
Recall & ranking
Two recall paths, never conflated: inline
— automatic inside turn(), on the raw user input, no query
authored by anyone — and manual — the explicit endpoints
with deliberate queries and explicit top_k. The inline path
runs two searches: one over deliberate memories (the
semantic record class) and one over conversation exchanges (the
temporal record class).
The exchange is the recall unit
Ranked results are deduplicated by exchange_pair_id, so
top-5 recall means five different past exchanges, never the same one
twice. Dedupe is by identity, not similarity: three near-identical
exchanges from three sessions have three pair ids and all three can
rank. Every recalled exchange then renders whole. Session-scoped replay
(/conversation/recall with a session_id) is
exempt from pair-dedupe and ranking: the session's rows return
newest-first by the time each was stored, whole, up to
top_k — replaying a long span means asking with a
top_k sized to it, within your subscription's ceiling.
# manual recall — each result carries its own ranking trace in metadata
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": "what did we decide about retry behavior?", "top_k": 5}'
# one returned record, abridged — why it ranked where it did:
# { "content": "Retries: exponential backoff, cap 3 attempts, decided 2026-07-30.",
# "metadata": {
# "semantic_relativity": 0.83, # raw similarity to this query
# "resonance_score": 0.91, # final ranking score
# "recall_count": 7, # times recall has returned it
# "tags": ["retries", "policy"], "exchange_pair_id": "xp-01a03987ae5a7c4392f1b06d2e9d51aa" } }
Budget & admission
budget_total is the authority: the compiler admits content
until the budget is spent.
What fills the window, in what order
Protected core admits first, unconditionally: your
operator_briefing and the current user message — the last
user-role entry in messages, so always send the message you
want protected — are in the packet whatever the budget. Everything else competes through
balanced admission across sources — each candidate item
belongs to one source, and sources are weighted:
| Source | What fills it | Priority |
|---|---|---|
conversation | The session's recent conversation, read back from what it stored, plus anything you send in messages; newest prioritized | Highest — the active conversation has the highest priority in the window |
semantic | Recalled deliberate memories | Second — durable knowledge relevant to this turn |
search | Recalled past exchanges, in reading order | Third — episodic context from other sessions and older turns |
tool | Tool-output items: tool.call.finished events when messages is sent as interface events (plain role: "tool" messages count as conversation in the packet but compete in the fixed lane — see the chat-host section) | Fourth — current-turn scaffolding |
fixed | Holds, artifacts_context, section markers, the status line | Admitted with the fixed buffer; holds are capped only by max_hold_items if you set one |
Weights bias, they don't ration: an empty source simply doesn't compete. With no recall to place, conversation takes all the space; a short conversation leaves the window to recall. Nothing is reserved for a source that has no candidates.
The meter
The status line reports the economy inside the packet every
turn — Context: 3210/8192 tokens | 2 holds active (350 tokens) |
... — where the model can read it and act on it.
result.token_count / budget_total report the
same numbers to your code, and they are the authoritative source for any
context meter: counting your own messages undercounts (it knows nothing
about recall), and your LLM provider's usage measures a different
tokenizer after your own additions.
# the budget dials on /turn (budget_total is required; recall_top_k is /turn-only)
{ "budget_total": 8192, "recall_top_k": 12 }
# what comes back, and what the model reads — the same numbers:
# result.token_count = 3210, result.budget_available = 4982
# status line inside the packet:
# "Context: 3210/8192 tokens | 0 holds active (0 tokens) | conversation: 310 tokens
# | sources: conversation=3, hivemind=4, system=3 | available: 4982 tokens"
A policy is an overlay: set only the
fields you mean to change — everything you omit keeps its budget-derived
value (max_context_tokens from budget_total,
balanced admission, budget-scaled caps):
# cap memories at 6 and holds at 2; the 8192 window and balanced
# admission carry over from budget_total automatically
{ "budget_total": 8192,
"policy": { "max_memory_items": 6, "max_hold_items": 2 } }
Building a chat host
The two-call loop is the whole integration contract — but a long-lived, restartable, streaming host needs a layer around it that the loop alone doesn't provide. This is that layer, stated at the wire level: your client keeps a little state, and these are its rules.
- Your client's state is two things: a turn counter
(increment per turn) and the open exchange (its
exchange_pair_idand turn number, held between the/turncall and its/conversation/store). No conversation history: the service stores every message and reads the session's recent conversation back on every/turn. Send only the new message asuser_input;messagesis for tool results only (see Tool use below). - Stream first, compile second.
/turndoes real work — a store, semantic recall, and a full context compile — and at large budgets that takes seconds, not milliseconds. If you call it before sending anything, streaming clients receive no bytes and many treat the connection as failed. Establish the stream first (headers or a keep-alive frame), then compile, then stream your model. - Restarts: nothing to rebuild — the conversation
lives in the service. Map your conversation id to
session_idso a restarted host lands in the same session; when a user clears a thread, start a new session id so the fresh thread doesn't inherit what they asked to be rid of. - Aborted generation. A dropped stream is the same as a cancel: discard the open exchange and move on. The one legitimate partial case is a host that deliberately keeps what was shown on screen as what was said, decided after the stream ends.
- Continuation hops:
rolemarks position in the exchange — which side initiated, which side answered — not human vs. machine. So deliberately feeding the model's own final output back in as the next/turn(inner monologue, staged reasoning, "keep going") is correct use: the prior output is the initiating utterance of the next remembered exchange, and recall will later find the exchange by it. Mid-turn tool cycles are NOT continuation hops — they stay inside one exchange (see Tool use below). - Tool use — the lifecycle first, authoritatively:
one user exchange is ONE
/turn, however many tool cycles happen inside it:
Never route a tool cycle through another/turn opens the exchange (stores the user input, returns the packet) → your model requests a tool → your host executes it → your host continues the model with the result # no Hivemind call → repeat, bounded /conversation/store records ONLY the final assistant reply — same exchange_pair_id and turn_number the /turn used/turn: every/turnstores itsuser_inputas a new exchange, so mid-loop calls write tool chatter into memory. Hivemind never calls your model and never sees tool definitions — how your host invokes tools is your host's business. Tool results are not stored by the service, so a later compile sees one only while you keep sending it inmessages. They enter the compile two ways. The simple way: plainrole: "tool"entries, carrying identity inmetadata— they count as conversation in the packet and render withtool_call_idandnameset:
The structured way: send# a tool result as a plain message — conversation-classified "messages": [ {"role": "tool", "content": "billing_lookup: $4,120.50 across 3 invoices", "timestamp": 1756500004, "metadata": {"tool_call_id": "call_7f3a", "tool_name": "billing_lookup"}} ]messagesas interface events (every entry{type, payload}; the list is one shape or the other, never mixed).tool.call.finishedevents become items in the dedicatedtooladmission source, governed bymax_tool_itemsandinclude_tools, with success and failure recorded on the item:
Either way, don't# a tool result as an interface event — tool-source classified, policy-capped "messages": [ {"type": "tool.call.finished", "id": "call_7f3a", "ts": 1756500004, "payload": {"call_id": "call_7f3a", "ok": true, "result": "billing_lookup: $4,120.50 across 3 invoices"}} ]/conversation/storetool chatter — the exchange worth remembering is the user's input (stored by/turn) and the assistant's final reply, so recall returns conclusions, not scaffolding. - Authorship: who produced a message is a
separate question from which side it sits on: pass
authoron/turnandadditional_metadata.authoron the store — identity as ordinary metadata, surfaced as the chat message'snamefield in compiles and recalls. Multi-agent transcripts stay attributed, and it filters:/recall_with_metadatawith{"author": ...}. - Turn-local instructions: where content enters
decides how long it lives.
artifacts_contextis included in one compile and never stored.operator_briefingis also compiled-only, but re-send it every turn — standing instructions and mode flags.holdsis a list of{id, title, content}objects (optional:tokens,held_since,priority— echoed into the rendered hold block as labels; the accounting'shold_tokensis measured by the compiler regardless) that you keep client-side and pass on every/turnuntil you drop them — pinned operational state ("stop-order", "API is down") that never enters memory: not stored, not recalled — visible only in the packet (and by item id in the compile receipt'soutputs_ref). Durable facts go to/create_memory— forever, recallable from any session. Pick the shortest lifetime that does the job; anything longer persists into future context where it no longer belongs. - Control tokens: Hivemind stores and returns text
verbatim — but recalled text re-enters your model's context later, and
that model's tokenizer may remove its own special tokens or interpret
them as control sequences. Strip or alter markers like
<|...|>before storing. - Failure severities differ. A
failed
/conversation/storemeans the chat already succeeded — never surface it to the user. What to do next depends on the answer: a 5xx, or a failure before any response began, is retried out-of-band with a backoff that ends; a 4xx other than 429 is the service refusing that request and will refuse it again unchanged — fix it or set it aside, and never let it hold up the stores queued behind it. A timed-out store is delivery-uncertain: the write may have landed, and the service does not dedupe a re-sent reply half — verify first: replay the session with/conversation/recalland look for the reply under itsexchange_pair_id, then re-send only if it is not there. - Agent self-service tools: beyond the automatic
recall inside
/turn, you can hand the agent deliberate memory operations as ordinary host-executed tools — the host maps tool names to endpoints, executes the HTTP call, and returns the response as a tool result. The results ride yourmessagesinto the next model call like any other tool output; nothing recalled this way is written back to memory — recall reads, only/create_memoryand the exchange stores write. Authorization is the host's: expose read tools freely, gate write and delete tools per agent (the service additionally plan-gates destructive endpoints server-side). Artifacts and holds are client-managed levers the agent can ask for: a "pin this" tool just adds to theholdslist your host sends each turn.
Three destinations, never confused: the same-turn tool result lives in your model provider's continuation transcript; what later compiles see is the stored conversation plus the tool results you keep sending in# host-side tool table — each endpoint tool is one HTTP call search_memory -> POST /recall_memory {"query", "top_k"} read search_by_metadata -> POST /recall_with_metadata {"query", "metadata", "top_k"} read search_episodes -> POST /conversation/recall {"query", "top_k"} read — cross-session replay_session -> POST /conversation/recall {"query", "session_id", "top_k"} read — one session, newest-first remember -> POST /create_memory {"content", "metadata"} write — gate it # client-side tools: no HTTP call — the host mutates its own per-turn state pin_context -> add a hold to the host's holds list release_context -> remove a hold by id (the rendered hold block tells the model exactly this: To release: context.release("id")) attach_artifact -> set artifacts_context for the NEXT compile only schedule/list/cancel -> the host's own scheduler, never a Hivemind call # one cycle: the agent searches its own memory mid-turn model → tool_call search_memory {"query": "retry policy decisions", "top_k": 5} host → POST /recall_memory → returns records host → continues model with the records as the tool result # the records are context for THIS turn only — not re-stored, not a new exchangemessages(plain or as interface events); andartifacts_contextreaches the next compile only — nothing changes a packet that has already been returned. Scheduled activation is host-owned — Hivemind has no scheduler. A cron event wakes your agent by opening an ordinary attributed exchange:/turnwith the trigger asuser_inputandauthornaming the scheduler ("cron"), stored and recallable like any other exchange.
Forking a conversation
To clone an agent so the clone continues from the same context, fork
the agent's session into a new one and run the clone on the new session
id. POST /conversation/fork with the session to fork and the
new session id:
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"}'
From that moment the new session's conversation is the parent's
conversation as it stood at the fork, followed by whatever the new
session stores. The clone's first /turn sends only its new
message, like any turn: the packet carries the parent's recent
conversation, and /conversation/recall with the new session
id replays it the same way.
Both sessions are in the same project, and the recall inside
/turn searches all of its sessions, the parent's and the
clone's included. Deleting the new session
(/delete_by_metadata with {"session_id": ...})
removes its marker record with its messages.
Fields and the response: POST /conversation/fork.
Projects and storage
Your memory lives in a Qdrant cluster you own — we
never hold your data. After subscribing, connect a cluster from the
dashboard: paste its URL and API key,
we validate it live (reachable, key accepted); the collection is
created automatically on your first request, and requests
start working within minutes. Until a cluster
is connected, every request answers 400 with
"No storage connected". Any Qdrant instance the service can
reach works: Qdrant
is open source, so you can self-host it on your own server or VPS
and pay nobody for storage, or use
Qdrant Cloud if you'd
rather not operate it — its free tier is enough to start. Every turn
round-trips to your cluster, so distance between the service and your
cluster is latency, and your cluster's capacity sets your throughput.
If your cluster is unreachable, requests fail closed rather than
writing anywhere else.
A workspace holds multiple projects, each bound to one cluster (the same cluster or different ones). Projects are hard data separation: each resolves to its own storage, so records in one project are never candidates for recall in another — separate agents, environments, or customers of yours can share a workspace without sharing memory. Credentials can be rotated and clusters moved per project at any time from the dashboard.
Which projects a key reaches is set by roles, the
same roles your workspace's members hold. Each project lists the roles
that reach it, under Projects in the dashboard. A key is
given All projects, or one role and
reaches every project that lists that role: list the role on another
project, or take it off one, and the key follows. A key cannot be given
the admin role; give a key that needs every project All projects.
Whichever it is, each request names one project in the header, and a
request naming a project the key does not reach is refused with
403. No request ever spans two projects. A key that reaches
no project — its role is listed on none — is refused with
403 on every request. A change takes about five minutes to
reach the API; a new key works straight away.
A key also carries what it may remove, set the same
way. Cannot delete is read and write only, and is what every new
key starts as. Can delete allows the recoverable deletes:
DELETE /memory/{id} and /delete_by_metadata.
Can delete and purge adds the permanent ones:
/purge_deleted_memories and
/purge_memories, which erases the whole workspace. A key
asked to do more than it may is refused with 403, and the
attempt is written to your receipts as
memory.destructive_refused, naming the route and the key.
A host should never find this out by trying: GET /usage
returns destructive_access.delete and
destructive_access.purge for the calling key, along with
the projects it can reach, and reading it writes nothing. Read it before asking a
person to approve a delete, and keep the answer for a few minutes
rather than reading it for every request — a change of level takes
about five minutes to reach the API in any case.
In a workspace several people or agents write to, give most keys
Cannot delete and keep purging for a key you hold yourself.
A key with no level set is not a usable key: every request is refused
as an invalid key, 401. Keys created in the dashboard always
have one.
# same key, different project — hard separation is one header
curl -s -X POST "$BASE/recall_memory" \
-H "Authorization: Bearer $HIVEMIND_API_KEY" \
-H "X-Hivemind-Project: proj_9f21ab04c3d2" \
-H "Content-Type: application/json" \
-d '{"query": "deploy checklist", "top_k": 5}'
# omit the header and the call is refused with 400 — there is no default
Errors and limits
| Status | Meaning & what's in details |
|---|---|
404 MEMORY_NOT_FOUND | Nothing relevant stored yet — normal for a new workspace; proceed and store what you learn. |
409 CONFLICT | The request conflicts with what is stored: /conversation/fork into a session id that already holds records. details.field names the field. Pick a new session id; the same request will be refused again |
429 RATE_LIMITED | The service is at capacity for this instant and refused the request rather than queue it. Subscriptions carry no requests-per-minute limit. The Retry-After header says exactly how long to sleep — use it instead of guessing a backoff. details.reason says which case: in_flight_ceiling (capacity, a couple of seconds), or pre_auth_ip (too many requests with a missing or invalid key from one address — requests with a valid key never count toward it) |
400 VALIDATION_ERROR | Malformed request: empty content, missing session id, an explicit recall_top_k above your subscription's ceiling, a budget_total above 128,000, or an oversized field (content and
user_input over 200,000 characters, queries over 8,000, more than
1,000 messages or 100 holds, or metadata beyond its
limits: 64 keys, keys of 128 characters, string values of 8,000 characters,
list values of 64 items). A
400 is permanent for that request: fix it or drop it, never retry
it unchanged. Whole requests over 8 MB are refused with 413 before
they are read. details.field names the offending field (the recall_top_k rejection instead carries max_top_k and requested_top_k). Also "No storage connected" (details.action = connect_cluster): your workspace is active but no Qdrant cluster is connected yet — connect one on the dashboard |
401 UNAUTHORIZED | Missing, invalid or revoked key, or an operation your subscription doesn't enable — the message names which. A key revoked in the dashboard, a device key whose person has left the workspace, and a key with no settings in the workspace are all "Invalid API key", never a 403. Revocations reach the API within ~5 minutes |
403 UNAUTHORIZED | The key is valid but may not do this; the message names which case. "Account is not active": the workspace is not active, and details.status says why — resubscribing brings every key back. "This key does not reach that project": the project named in X-Hivemind-Project is not one the key reaches — name one it reaches, or list the key's role on this project in the dashboard. "This key reaches no project": its role is listed on no project — list the role on a project, or give the key another role or All projects, in the dashboard. "This key may not delete records" or "may not purge records": the key's level does not allow the route — details.required names the level needed and details.key_level the one the key has; raise it in the dashboard. A 403 never succeeds on retry |
5xx / timeout | Service or storage trouble. A /turn whose message could not be stored answers 500 and compiles nothing: the message has no stored row, so the conversation would be missing it. Your call whether to fail open (proceed with local context only) or fail closed — decide once, document it in your host |
All errors arrive as one machine-readable envelope shape:
{"ok": false, "error": {"code", "message", "details"}, "request_id"}.
The code values are stable strings (branch on them, not on
messages), and request_id is the correlator to quote in a
support request — it ties your call to our logs and receipts.
# a 429, exactly as it arrives
HTTP/1.1 429 Too Many Requests
Retry-After: 2
{"ok": false,
"error": {"code": "RATE_LIMITED", "message": "Service is at capacity for this instant — retry shortly.", "details": {"reason": "in_flight_ceiling"}},
"request_id": "0198c7a2-59f2-711e-8d2e-0242ac120002"}
# branch on error.code; sleep exactly Retry-After seconds; quote request_id to support