Save a record. That's the setup.
There is no schema to declare. The first write creates the database and collection. Install @monkeyhub/sdk (no runtime dependencies) or call REST directly.
PUT /v1/app/memories/records/memory_1
{
"data": {
"id": "memory_1",
"agentId": "agent_7",
"status": "active",
"text": "The user prefers metric units",
"priority": 2
},
"ttl": "2027-01-01T00:00:00Z"
}{
"data": {
"id": "memory_1",
"agentId": "agent_7",
"status": "active",
"text": "The user prefers metric units",
"priority": 2
},
"updatedAt": 1790000000000,
"updatedBy": { "type": "agent", "id": "agt_..." },
"ttl": 1798761600
}Bearer API keys.
Send Authorization: Bearer mk_live_... on every request. Keys belong to one org and carry permissions: * (owner), db:* (every database, issued to approved agents), db:app:rw, or db:app:ro. Out-of-scope calls return 403 db_scope_violation. Create keys in the console; revocation applies within 60 seconds.
curl https://api.monkeyhub.ai/whoami \
-H "Authorization: Bearer mk_live_..."
{
"orgId": "org_3kTq...",
"actor": { "type": "agent", "id": "agt_..." },
"plan": "free",
"permissions": ["db:*"]
}JSON objects with an id.
REST wraps each record in an envelope so metadata never collides with your fields: data holds the record, updatedAt is server-assigned epoch milliseconds, updatedBy is who last saved or patched the record (the owner or an agent, by ID), and ttl is optional expiry (epoch seconds, or an ISO-8601 timestamp on input). updatedAt, updatedBy, and ttl cannot be record fields.
Pass the updatedAt you read as ifUpdatedAt on save, patch, or delete; if someone wrote first you get 409 conflict with the current value, so re-read, merge, and retry. Patch merges top-level fields and null removes a field. The SDK and MCP return flat records with updatedAt and updatedBy merged in.
PATCH /v1/app/memories/records/memory_1
{
"data": {
"status": "archived",
"text": null
},
"ifUpdatedAt": 1790000000000
}Filter and sort on any field.
where maps a field or dotted path (like profile.city) to a value for equality, or to operators: eq ne gt gte lt lte in beginsWith exists. Comparisons are type-strict, so 2 does not match "2"; ne and exists: false also match records missing the field.
Without orderBy, results are newest first. With it (id, updatedAt, or any field), order defaults to asc. Pass the returned cursor with the same query for the next page; cursor: null means done.
POST /v1/app/memories/query
{
"where": {
"agentId": "agent_7",
"status": {
"in": [
"active",
"pinned"
]
},
"priority": {
"gte": 2
}
},
"orderBy": "priority",
"order": "desc",
"limit": 20
}{
"items": [
{
"data": { "id": "memory_1", "agentId": "agent_7", "status": "active", "priority": 2 },
"updatedAt": 1790000000000
}
],
"count": 1,
"cursor": null
}Atomic, all or nothing.
Up to 100 save, patch, and delete operations in one collection apply together. Any failure aborts the whole batch and error.details.index names the operation.
POST /v1/app/memories/batch
{
"ops": [
{
"op": "save",
"data": {
"id": "memory_2",
"agentId": "agent_7",
"status": "active"
}
},
{
"op": "patch",
"id": "memory_1",
"data": {
"status": "active"
}
},
{
"op": "delete",
"id": "memory_3"
}
]
}Optional, for speed only.
Every field is queryable without setup; unindexed filters and sorts scan the collection. Once a collection passes roughly 100,000 records, index the fields you filter or sort on (up to 10). The list replaces the previous set, and indexes never change results.
PUT /v1/app/memories
{
"indexes": [
"agentId",
"status"
]
}Rewind any collection 30 days.
Every collection keeps 30 days of history. A rewind restores records, TTLs, indexes, and counts together to the nearest recorded point, and returns 202 with an undo token. The collection restarts within a few seconds and answers 409 rewind_in_progress until then; confirm by reading data back.
POST /v1/app/memories/rewind
{
"to": "2026-10-01T12:00:00Z"
}
// 202
{ "status": "rewinding", "to": 1790000000000, "undo": "<token>" }POST /v1/app/memories/rewind
{ "undo": "<token>" }The catalog (collection name, soft-delete state) and API keys do not rewind, and updatedAt keeps increasing, so concurrency tokens from abandoned history never match new writes.
Databases, collections, and safe deletes.
Databases are namespaces for collections, key scopes, and settings. Deleting a collection is a soft delete: it disappears immediately, its name stays reserved, and POST .../undelete restores it for 7 days before it is purged. Turn on deletion protection for production databases; while on, deletes return 409 database_protected, and only the owner can turn it off.
PUT /v1/app
{
"environment": "production",
"deletionProtection": true,
"description": "Agent memory"
}Endpoint reference.
| Method | Path | Purpose | Notes |
|---|---|---|---|
| GET | /v1 | List databases | Databases visible to the key. |
| PUT | /v1/:db | Configure database | { environment?, deletionProtection?, description? }; creates it if needed. |
| GET | /v1/:db | List collections | limit, cursor, includeDeleted. |
| DELETE | /v1/:db | Delete database | Soft-deletes every collection; blocked by deletion protection. |
| PUT | /v1/:db/:collection | Create collection / set indexes | { indexes? } replaces the index set; idempotent. |
| GET | /v1/:db/:collection | Collection details | indexes, recordCount, storageBytes, rewoundAt. |
| DELETE | /v1/:db/:collection | Delete collection | Soft delete, restorable for 7 days. |
| POST | /v1/:db/:collection/undelete | Undelete | Restores a soft-deleted collection. |
| POST | /v1/:db/:collection/rewind | Rewind | { to } within 30 days, or { undo }; 202. |
| PUT | /v1/:db/:collection/records/:id | Save record | { data, ttl?, ifUpdatedAt? }; creates the collection on first write. |
| GET | /v1/:db/:collection/records/:id | Get record | { data, updatedAt, updatedBy?, ttl? } or 404. |
| PATCH | /v1/:db/:collection/records/:id | Patch record | Merges top-level fields; null removes a field. |
| DELETE | /v1/:db/:collection/records/:id | Delete record | 204; ?ifUpdatedAt= for a conditional delete. |
| POST | /v1/:db/:collection/query | Query | { where?, orderBy?, order?, limit?, cursor? }. |
| POST | /v1/:db/:collection/batch | Batch | Up to 100 save/patch/delete ops, atomic. |
| GET | /whoami | Who am I | The org, actor, plan, and permissions your key resolves to. |
Errors use one envelope: { error: { code, message, details, hint? }, requestId }. Follow hint when present.
REST, MCP, or the TypeScript SDK.
Agents connect to the streamable HTTP MCP server at https://api.monkeyhub.ai/mcp with the same Bearer key; its tools mirror the REST operations. The full reference for agents lives at https://api.monkeyhub.ai/SKILL.md, indexed by https://monkeyhub.ai/llms.txt.
const memories = monkey.db("app").collection<Memory>("memories");
await memories.get("m1"); // flat record, or null
await memories.patch("m1", { status: "done" }, { ifUpdatedAt: m.updatedAt });
for await (const memory of memories.queryAll({ agentId: "a7" })) {
console.log(memory.text);
}
await memories.batch([{ op: "delete", id: "m2" }]);
await memories.configure({ indexes: ["agentId"] });
await memories.drop(); // soft delete
await memories.undelete();{
"mcpServers": {
"monkeyhub": {
"url": "https://api.monkeyhub.ai/mcp",
"headers": { "Authorization": "Bearer ${MONKEYHUB_API_KEY}" }
}
}
}Agents can ask for their own key.
An agent names its owner's email and a request waits in the owner's console for up to 30 days. Approval issues a db:* key, which never grants billing or org settings. With an allowlisted HTTPS callbackUrl, approval is delivered as a signed webhook (X-MonkeyHUB-Signature) carrying a single-use, 24-hour claim link; otherwise the owner shares the key.
POST https://api.monkeyhub.ai/signup/agent
{
"ownerEmail": "human@example.com",
"provider": "claude",
"agentName": "research-runner"
}
// 202
{ "pending": { "pendingId": "pnd_...", "expiresAt": "..." } }