Developer docs

Build on MonkeyDB.

Store JSON documents with an id, query them by any field, and rewind any collection to any moment in the last 30 days. One API at api.monkeyhub.ai for REST, MCP, and the SDK.

api.monkeyhub.ai
Base URL
10,000 ops
Free tier
30 days
Rewind
quickstart.ts
import { MonkeyHubClient } from "@monkeyhub/sdk";

const monkey = new MonkeyHubClient({
  apiKey: process.env.MONKEYHUB_API_KEY!
});
const memories = monkey.db("app").collection("memories");

// The first write creates the database and collection.
const m = await memories.save(
  { id: "memory_1", agentId: "agent_7", status: "active" },
  { ttl: "2027-01-01T00:00:00Z" }
);

const page = await memories.query(
  { agentId: "agent_7", status: { in: ["active", "pinned"] } },
  { orderBy: "updatedAt", order: "desc", limit: 20 }
);

// Undo a mistake: rewind the collection an hour.
const { undo } = await memories.rewind({ to: new Date(Date.now() - 3_600_000) });
Quickstart

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.

Save (REST)
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"
}
Response
{
  "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
}
Authentication

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.

Who am I
curl https://api.monkeyhub.ai/whoami \
  -H "Authorization: Bearer mk_live_..."

{
  "orgId": "org_3kTq...",
  "actor": { "type": "agent", "id": "agt_..." },
  "plan": "free",
  "permissions": ["db:*"]
}
Records

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
PATCH /v1/app/memories/records/memory_1

{
  "data": {
    "status": "archived",
    "text": null
  },
  "ifUpdatedAt": 1790000000000
}
Query

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.

Query
POST /v1/app/memories/query

{
  "where": {
    "agentId": "agent_7",
    "status": {
      "in": [
        "active",
        "pinned"
      ]
    },
    "priority": {
      "gte": 2
    }
  },
  "orderBy": "priority",
  "order": "desc",
  "limit": 20
}
Response
{
  "items": [
    {
      "data": { "id": "memory_1", "agentId": "agent_7", "status": "active", "priority": 2 },
      "updatedAt": 1790000000000
    }
  ],
  "count": 1,
  "cursor": null
}
Batch

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.

Batch
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"
    }
  ]
}
Indexes

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.

Set indexes
PUT /v1/app/memories

{
  "indexes": [
    "agentId",
    "status"
  ]
}
Recovery

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.

Window
last 30 days, not the future
Undo
POST { undo } · token valid 29 days
Cost
never metered · requires rw
Rewind
POST /v1/app/memories/rewind

{
  "to": "2026-10-01T12:00:00Z"
}

// 202
{ "status": "rewinding", "to": 1790000000000, "undo": "<token>" }
Undo the rewind
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.

Lifecycle

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.

Protect a database
PUT /v1/app

{
  "environment": "production",
  "deletionProtection": true,
  "description": "Agent memory"
}
REST

Endpoint reference.

MethodPathPurposeNotes
GET/v1List databasesDatabases visible to the key.
PUT/v1/:dbConfigure database{ environment?, deletionProtection?, description? }; creates it if needed.
GET/v1/:dbList collectionslimit, cursor, includeDeleted.
DELETE/v1/:dbDelete databaseSoft-deletes every collection; blocked by deletion protection.
PUT/v1/:db/:collectionCreate collection / set indexes{ indexes? } replaces the index set; idempotent.
GET/v1/:db/:collectionCollection detailsindexes, recordCount, storageBytes, rewoundAt.
DELETE/v1/:db/:collectionDelete collectionSoft delete, restorable for 7 days.
POST/v1/:db/:collection/undeleteUndeleteRestores a soft-deleted collection.
POST/v1/:db/:collection/rewindRewind{ to } within 30 days, or { undo }; 202.
PUT/v1/:db/:collection/records/:idSave record{ data, ttl?, ifUpdatedAt? }; creates the collection on first write.
GET/v1/:db/:collection/records/:idGet record{ data, updatedAt, updatedBy?, ttl? } or 404.
PATCH/v1/:db/:collection/records/:idPatch recordMerges top-level fields; null removes a field.
DELETE/v1/:db/:collection/records/:idDelete record204; ?ifUpdatedAt= for a conditional delete.
POST/v1/:db/:collection/queryQuery{ where?, orderBy?, order?, limit?, cursor? }.
POST/v1/:db/:collection/batchBatchUp to 100 save/patch/delete ops, atomic.
GET/whoamiWho am IThe org, actor, plan, and permissions your key resolves to.

Errors use one envelope: { error: { code, message, details, hint? }, requestId }. Follow hint when present.

Agents

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.

TypeScript SDK
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();
MCP client config
{
  "mcpServers": {
    "monkeyhub": {
      "url": "https://api.monkeyhub.ai/mcp",
      "headers": { "Authorization": "Bearer ${MONKEYHUB_API_KEY}" }
    }
  }
}
Agent signup

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.

Request access
POST https://api.monkeyhub.ai/signup/agent

{
  "ownerEmail": "human@example.com",
  "provider": "claude",
  "agentName": "research-runner"
}

// 202
{ "pending": { "pendingId": "pnd_...", "expiresAt": "..." } }
Limits

Operational rules.

Names
Databases and collections: [a-z0-9_-]{1,64}; names starting with _ are reserved
Record ids
1–256 characters of A–Z a–z 0–9 _ - . : @, not starting with . : or @
Record size
256 KiB of JSON; request bodies up to 1 MiB
Queries
Up to 10 where fields, 100 in values, field paths up to 5 segments; limit 50 by default, 100 max
Indexes
10 per collection
Batch
100 operations in one collection
Collections
Up to 10 GB and roughly 1,000 requests per second each
Rate limit
1,000 requests per 10 seconds per org (429 rate_limited with Retry-After)
Rewind
Any moment in the last 30 days; undo tokens last 29 days
Plans
Free 10,000 ops/month; Pro 1,000,000; Ultra 25,000,000