# viasocket-memory guide

Give your AI a memory. It remembers how each user likes things done, what they told it about their work, what a team or customer account knows, and what it learned when someone corrected it. It finds the right memories before each answer, and it never mixes up one user's memory with another's.

viaSocket gives your backend one thing: a memory token for the user. Everything else is a call to the memory service with that token. Start with the level that matches what you are building, and stop there. Memory service replies shown here come from a real run on 22 Oct 2026, shortened where marked `…`; names and ids are examples.

**For AI agents and coding assistants:** this guide as plain Markdown is at `https://memory-api.viasocket.com/guide.md` (no token needed). Read it whole before building.

## Pick your level

| You want | Level | What it takes |
| --- | --- | --- |
| Your AI to remember each user | [1. Give your AI a memory](#level-1-give-your-ai-a-memory) | A token, then search before answering and learn after |
| Users to see, fix and delete what is remembered | [2. Let users control it](#level-2-let-users-see-and-control-their-memory) | A few more calls |
| To save exact facts, documents, or lessons from corrections | [3. More ways to remember](#level-3-more-ways-to-remember) | One call each |
| Memory shared by a team, an account or several agents; agents that use memory as tools | [4. Sharing and agents](#level-4-sharing-agents-and-tools) | Spaces in the embed token, MCP |

Further down: [Reference](#reference) · [Recipes](#recipes) · [Errors and limits](#errors-and-limits) · [How it compares](#how-it-compares-to-mem0-supermemory-letta-zep-and-pastdev)

## The ideas in one minute

| Idea | What it is | Example |
| --- | --- | --- |
| **Space** | A folder of memories. Each user has their own; you can add shared ones. | `users/18231` (one user), `teams/acme`, `customers/bloom.com`, `agents/support` |
| **Note** | One memory: a short fact, a rule, a lesson or a whole document. | "Acme uses HubSpot as its CRM." |
| **Rule** | A note about how to work ("keep a casual tone"). Rules are applied on every answer; facts are used when they are relevant. | `rules/tone` |
| **Version** | Changing a note keeps the old version. You can read memory as it was on any date. | `pricing/plans` at version 1, 2 and 3 |
| **Token** | Says who is asking and which spaces they may `read`, `append` to (add, never overwrite others) or `write`. | `{ "users/18231": "write", "teams/acme": "append" }` |

Every note is found by meaning, by exact words, and by the names in it (people, companies, products) and how they connect. You never manage that index.

### What you get without building anything

- **Search by meaning, words and names at once.** Every note is embedded (vector search), indexed by its exact words (keyword search), and read for the people, companies, products and apps it names and how they link ("acme uses hubspot"), which forms a graph. One search uses all three, and also brings up what the named things are connected to.
- **Learning from conversations.** Send a chat and a model picks out what lasts, sorts rules from facts, merges with what is already known (a repeat confirms, a change updates, "forget that" archives) and drops small talk and secrets.
- **Rules, facts and lessons kept apart.** Rules apply to every answer, facts when they are relevant, lessons when a similar task comes up.
- **A ready prompt block.** Recall returns a block to paste into your AI's system prompt: rules first, then dated facts, connections and lessons, kept short.
- **Every version kept.** Read memory as it was on any date; see who changed what.
- **Documents.** Web pages, PDFs, Google Docs and YouTube transcripts become searchable notes.
- **Access by token.** Each user's own space is theirs alone; shared spaces are granted per token as read, append or write, on every call including MCP.

## Level 1: give your AI a memory

### Get a memory token

Your backend already has the user's viaSocket embed token. Exchange it for a memory token:

```bash
curl "https://flow-api.viasocket.com/embed/ai/memory-token" -H "Authorization: <embed token>"
```

```json
{ "success": true, "message": "Memory token issued", "data": {
  "token": "<memory token>",
  "url": "https://memory-api.viasocket.com",
  "mcp_url": "https://memory-api.viasocket.com/mcp",
  "expires_in": 3600,
  "me": "users/18231",
  "spaces": { "users/18231": "write", "teams/acme": "append" } } }
```

`me` is the user's own space, and `spaces` is everything the token reaches (shared spaces come from the embed token, see [Level 4](#level-4-sharing-agents-and-tools)). The token lasts an hour (`?expires_in=` up to 86400 seconds); get a new one when it runs out. Keep the embed token on your server, and hand the memory token to your own frontend or agent only when it needs one.

The examples below use:

```bash
export MEM=https://memory-api.viasocket.com
export TOKEN=<memory token>
```

**From your server (Node.js).** Use the `viasocket-apps` package (0.8 or later), which a product built with the viaSocket skill already calls everything through. It signs the embed token from `VIASOCKET_ORG_ID`, `VIASOCKET_PROJECT_ID` and `VIASOCKET_EMBED_SECRET`, exchanges it, keeps each user's memory token until a minute before it runs out (and takes a new one if the service refuses it), and makes every memory call:

```js
// memory.js, server only
import { viasocket } from './viasocket.js' // new ViaSocket({ orgId, projectId, secret }) from viasocket-apps@^0.8.0

// The shared spaces each user's token reaches (Level 4), decided by your product. Return the same grants for a user
// on every call. Without shared memory, return undefined.
export const memorySpacesOf = (userId) => undefined
// For example: (userId) => ({ 'teams/acme': 'append', 'product-truth': 'read' })

// One scope per user, so its memory token is reused across calls.
const users = new Map()
export const userOf = (userId) => {
  const id = String(userId)
  if (!users.has(id)) users.set(id, viasocket.user(id, { memorySpaces: memorySpacesOf(id) }))
  return users.get(id)
}

// Any memory call: memory(userId, 'POST', '/search', { query }). Returns the service's reply; a refusal throws a
// ViaSocketError with the service's status and message.
export const memory = (userId, method, path, body) => userOf(userId).memory.call(method, path, body)

// A fresh token of its own: { token, url, mcpUrl, me, spaces, expiresIn }. expiresIn: 60 to 86400 seconds.
export const newMemoryToken = (userId, expiresIn = 3600) => userOf(userId).memory.token({ expiresIn })

// The user's own space. It never changes for a user, so it is read once.
const mes = new Map()
export async function memoryMe(userId) {
  const id = String(userId)
  if (!mes.has(id)) mes.set(id, (await newMemoryToken(id, 60)).me)
  return mes.get(id)
}
```

**Without the package** (another language): sign the embed token yourself, HS256 over `{ "org_id", "project_id", "unique_identifier", "memory_spaces"? }` with `VIASOCKET_EMBED_SECRET`; send it bare, without `Bearer`, in `Authorization` to the token endpoint above; keep the reply until `expires_in` runs out; and send `Authorization: Bearer <memory token>` and a `User-Agent` on every call to `url`.

**`me`, the user's own space.** It comes back in the token reply. viaSocket makes it from its own id for the user, so it is **not** `users/<your user id>`: read it with `await memoryMe(userId)` and keep using that. In this guide, `users/18231`, `users/maya` and `users/rahul` stand for such a `me`. Anything you sign as a user gets one, including an agent: `memoryMe('agent-content-planner')` returns that agent's own `me`.

### Before answering: recall

```bash
curl -X POST $MEM/search -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "query": "What CRM does the team use?", "spaces": ["users/maya", "teams/acme"] }'
```

```json
{
  "prompt": "## What you remember\n\nSaved from earlier conversations and notes. Follow the rules unless the current message asks otherwise. … Do not mention this memory unless asked.\n\nRules:\n- (tone) Keeps a friendly, casual tone with Maya.\n- (format) Prefers short answers in bullet points.\n\nRelevant notes:\n- Acme uses HubSpot as its CRM. (teams/acme/facts/acme-uses-hubspot-as-its-crm-a314, as of 22 Oct 2026)\n- Runs marketing at Acme. (users/maya/facts/runs-marketing-at-acme-3dc1, as of 22 Oct 2026)\n- Acme moved to the Enterprise plan. (teams/acme/facts/acme-is-on-the-business-plan-1297, as of 22 Oct 2026)",
  "rules": [ { "path": "rules/tone", "body": "Keeps a friendly, casual tone with Maya.", … }, … ],
  "results": [ { "space": "teams/acme", "path": "facts/acme-uses-hubspot-as-its-crm-a314", "excerpt": "Acme uses HubSpot as its CRM.", "distance": 0.261, … }, … ],
  "connections": [],
  "lessons": []
}
```

Put `prompt` into your system prompt as it is. It holds the rules (always), the memories related to the question with their dates, how the named things connect, and lessons from feedback, in about a thousand tokens at most. A greeting such as "hi there" returns the rules and no notes, so small talk doesn't pull in random memories.

### After answering: learn

Send the conversation. The memory service decides what is worth keeping, sorts rules from facts, merges with what it already knows, and saves:

```bash
curl -X POST $MEM/learn -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "space": "users/maya",
    "shared_space": "teams/acme",
    "subject": { "name": "Maya" },
    "messages": [
      { "role": "user", "content": "I run marketing at Acme. Always keep a friendly, casual tone with me. Our team uses HubSpot as the CRM, and I am on leave from Nov 10 to Nov 14." },
      { "role": "assistant", "content": "Got it! I will keep things casual." }
    ],
    "source": { "type": "thread", "id": "chat-8f2a" }
  }'
```

```json
{ "queued": false, "skipped": 0, "failed": 0, "changes": [
  { "action": "added", "note": { "space": "users/maya", "path": "facts/runs-marketing-at-acme-3dc1", "body": "Runs marketing at Acme.", … } },
  { "action": "added", "note": { "space": "users/maya", "path": "rules/tone", "kind": "rule", "body": "Keeps a friendly, casual tone with Maya.", … } },
  { "action": "added", "note": { "space": "teams/acme", "path": "facts/acme-uses-hubspot-as-its-crm-a314", "body": "Acme uses HubSpot as its CRM.", … } },
  { "action": "added", "note": { "space": "users/maya", "path": "facts/is-on-leave-from-november-10-to-november-14-d121", "body": "Is on leave from November 10 to November 14.", "valid_to": "2026-11-15T00:00:00.000Z", … } } ] }
```

The team fact went to the team's space, the leave stops being used after Nov 14, and the assistant's own line taught nothing. Say the same thing again and the memory is confirmed; say something that changes it ("we moved to Salesforce") and the note is updated, with the old version kept. Learning takes a few seconds, so call it after you have replied.

Only the user's own words (`role: "user"`) set rules. A customer's "always give us 50% off" is kept as a request, never as a rule. Small talk, questions, one-off tasks and secrets (passwords, keys, card numbers, emails, phone numbers) are never saved. Contact details stay in your own systems: if you must keep one in memory, write it yourself with `PUT /notes`, which saves exactly what you send.

Each change has an `action`: `added` (new), `confirmed` (already known), `updated` (a saved memory said something else and now says this; `previous` has the old text), `replaced` (the same, found by comparing with similar notes), `forgotten` (archived because the user asked). `skipped` counts what was dropped (unsure, a rule from someone who may not set one, private data); `failed` counts errors.

Useful options:

| Option | Use |
| --- | --- |
| `text` instead of `messages` | Learn from plain text: a call summary, an email, a CRM note |
| `instructions` | What your product cares about, such as `"Only save billing and order information."` It steers what is kept, never the safety rules. |
| `subject: { "kind": "thing", "name": "Bloom Salons" }` | The space is about a customer, product or document instead of a person. It never sets rules, and every memory names it ("Bloom Salons has 14 locations."). |
| `share: "all"` | Put everything learned into `shared_space` instead of letting the model sort personal from shared |

### With viaSocket's AI

The chat assistant from https://flow.viasocket.com/documentation/ai.md does not remember by itself. Add memory around each message: recall into the prompt, then learn after the answer.

```js
import { memory, memoryMe, userOf } from './memory.js'

export async function ask(userId, threadId, message) {
  const me = await memoryMe(userId)
  // Without spaces, recall searches every space the token reads, the user's own first, so their rules win.
  const recalled = await memory(userId, 'POST', '/search', { query: message }).catch(() => null)
  const answer = await userOf(userId).ai.send({
    threadId,
    message,
    prompt: [BASE_PROMPT, recalled?.prompt].filter(Boolean).join('\n\n'),
  })
  // Learn after answering; memory failing never blocks the answer.
  memory(userId, 'POST', '/learn', {
    space: me,
    // shared_space: 'teams/acme', // with shared memory: team facts go there, personal ones stay in me
    messages: [{ role: 'user', content: message }, { role: 'assistant', content: answer.content }],
    source: { type: 'thread', id: answer.threadId },
  }).catch(() => {})
  return answer
}
```

## Level 2: let users see and control their memory

Users trust memory they can see. With the same memory token your backend can list what is remembered, let users correct it, and forget on request.

**Show what is remembered** (a "Your memory" page):

```bash
curl "$MEM/index?space=users/18231" -H "Authorization: Bearer $TOKEN"
```

```json
{ "spaces": ["users/18231"], "path": null, "folders": ["facts", "rules"], "notes": [
  { "path": "rules/tone", "kind": "rule", "summary": "Keeps a friendly, casual tone with Maya.", "updated_at": "2026-10-22T09:34:53.304Z", … },
  { "path": "facts/runs-marketing-at-acme-3dc1", "kind": "fact", "summary": "Runs marketing at Acme.", "source": { "type": "thread", "id": "chat-8f2a" }, … },
  { "path": "facts/is-on-leave-from-november-10-to-november-14-d121", "summary": "Is on leave from November 10 to November 14.", "valid_to": "2026-11-15T00:00:00.000Z", … } ] }
```

**Let them fix one** (each change is kept as a new version):

```bash
curl -X PATCH $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "users/18231", "path": "rules/tone", "ops": [{ "op": "str_replace", "old": "casual", "new": "formal" }] }'
```

**Let them delete one, a conversation, or everything:**

```bash
# one memory
curl -X DELETE $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "users/18231", "path": "facts/runs-marketing-at-acme-3dc1" }'
# { "deleted": 1 }

# everything learned from one conversation (when the user deletes the thread)
curl -X DELETE $MEM/source -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "type": "thread", "id": "chat-8f2a" }'
# { "source": { "type": "thread", "id": "chat-8f2a" }, "deleted": <how many> }

# "forget me": the user's whole space
curl -X DELETE $MEM/spaces -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "users/18231" }'
# { "space": "users/18231", "deleted": <how many> }
```

Pass `source: { "type": "thread", "id": "<your thread id>" }` when you learn, and forgetting a conversation is one call. Deleting is permanent; `"archive": true` on `DELETE /notes` stops using a note but keeps it in its history.

## Level 3: more ways to remember

### Which call to save with

| You have | Use | What happens |
| --- | --- | --- |
| A conversation, a chat turn, an email, call notes | `POST /learn` | A model picks what lasts, sorts rules from facts, merges with what is known |
| One fact that may repeat or change ("Acme moved to Enterprise") | `POST /notes` | Merged: a repeat confirms, a change replaces the old version |
| Many separate records (tickets, accounts, wiki pages, rows from your database) | `PUT /notes` at a path from the record's id, such as `customers/acme3.com/plan` | Saved exactly as sent; saving it again updates that path. Never merged with anything |
| A note you maintain (task status, product truth, a brief) | `PUT /notes` at a fixed path, then `PATCH /notes` to edit | You own the path and the content |
| A rule for the assistant | `POST /notes` with `kind: "rule"` and a `slot` | One live rule per slot; a new one replaces it |

`POST /notes` with `merge: false` also saves exactly what was sent, at a generated path. Use the merging save only for facts where "the same thing said again" should count as a repeat.

### Save one thing yourself

When you know exactly what to keep (a "Remember this" button, a CRM sync), send it. It is still merged with what the space knows:

```bash
curl -X POST $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "teams/acme", "text": "Acme moved to the Enterprise plan.", "kind": "fact", "labels": ["billing"] }'
```

```json
{ "outcome": "replaced",
  "note": { "path": "facts/acme-is-on-the-business-plan-1297", "version": 2, "body": "Acme moved to the Enterprise plan.", … },
  "previous": { "path": "facts/acme-is-on-the-business-plan-1297", "text": "Acme is on the Business plan." } }
```

`outcome` is `added` (new), `confirmed` (already known; its `confirmed` count goes up) or `replaced` (a change to a known memory, saved as its next version). A rule takes a `slot`, and a new rule in the same slot replaces the old one: `{ "text": "Prefers short answers in bullet points.", "kind": "rule", "slot": "format" }`.

### Keep one task's context together

When one user works on several things at once (a billing migration, a launch, a hiring round), give each task its own folder and recall only that folder while working on it:

```bash
# what is known about the task, written where it belongs
curl -X PUT $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "users/18231", "path": "tasks/billing-migration/deadline", "body": "The billing migration deadline is 10 December." }'

# recall for that task only: its notes, plus the user's rules and lessons
curl -X POST $MEM/search -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "query": "What is left for the billing migration?", "spaces": ["users/18231"], "path": "tasks/billing-migration" }'
```

`PUT /notes` and `PATCH /notes` write exactly what you send at that path, so a task's notes never merge with anything else. Facts learned from chat (`POST /learn`) and saved with `POST /notes` land in `facts/`; search without `path` still finds everything. `GET /index?space=users/18231&path=tasks/billing-migration` lists the task, and `DELETE /notes` on its notes clears it when the task is done.

### Documents your AI should answer from

Help articles, policies, PDFs, Google Docs, YouTube videos and web pages:

```bash
curl -X POST $MEM/documents -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "product-truth", "path": "docs/example", "url": "https://example.com/" }'
```

```json
{ "note": { "path": "docs/example", "kind": "doc", "version": 2, "summary": "This domain is for use in documentation examples without needing permission.", … },
  "document": { "url": "https://example.com/", "status": "indexed", "bytes": 156, … } }
```

The reply is `202` with `document.status` `queued`: the page is loaded and indexed after the reply, usually within seconds, a long PDF within a minute or two. `GET /open?space=…&path=…` shows `document.status` moving to `indexed` or `error`. A long `content` answers `indexing` the same way; a short one is `indexed` at once. Or send the text itself with `content` instead of `url`. Searches find documents like any memory, chunk by chunk; `"kinds": ["doc"]` searches documents only. Only public web addresses are fetched.

### Lessons from feedback

```bash
curl -X POST $MEM/feedback -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "agents/support", "task": "Classify the ticket: \"My refund for invoice 42 has not arrived.\"", "output": "technical", "outcome": "corrected", "correction": "billing" }'
```

```json
{ "changes": [ { "action": "added", "note": { "kind": "lesson",
  "body": "Tickets about refunds, invoices, payments, or charges are billing, even when they also mention delays or errors.", … } } ] }
```

The next similar task ("I was charged twice this month") gets the lesson in its recall `prompt`, as long as the lesson's space is among the spaces searched (leaving out `spaces` searches them all). Confirming a lesson again makes it `reinforced`; a correction that changes it makes it `revised`.

## Level 4: sharing, agents and tools

### Shared spaces

Each user always gets their own space. To share memory, grant more spaces in the embed token your backend signs, with the `memory_spaces` claim:

```js
// With viasocket-apps (memorySpacesOf in memory.js returns this for maya)
viasocket.user('maya', { memorySpaces: { 'teams/acme': 'append', 'product-truth': 'read', 'customers/*': 'read' } })

// Signing it yourself, the same claim
jwt.sign({
  org_id: "752",
  project_id: "proje2b9RoWO",
  unique_identifier: "maya",
  memory_spaces: { "teams/acme": "append", "product-truth": "read", "customers/*": "read" }
}, VIASOCKET_EMBED_SECRET, { algorithm: "HS256" });
```

| Access | May |
| --- | --- |
| `read` | Search, list and open |
| `append` | Also add memories and change their own, but never overwrite or delete what someone else saved |
| `write` | Everything, including changing and deleting anyone's memories in that space |

`"customers/*"` covers every space under `customers/`. Up to 20 spaces; names are letters, digits and `_ . @ -`, joined by `/`. Without the claim, a token reaches only the user's own space. Use `append` for most members of a shared space and `write` for its owners. Deleting and archiving, even of your own notes, need `write`: in a space it may only append to, an agent asks a person (or a `write` token) to clean up, for example to delete a bad run with `DELETE /source`.

Common setups:

| Goal | Grant | Then |
| --- | --- | --- |
| A team shares what it knows | each member `{ "teams/acme": "append" }` | `learn` with `shared_space: "teams/acme"`: personal things stay in the user's space, team facts go to the team |
| Product knowledge everyone reads | users `{ "product-truth": "read" }`, your admin tool `"write"` | The admin tool writes notes and documents; every recall can use them |
| One memory per customer account | the support agent `{ "customers/bloom.com": "write" }`, sales `"read"` | Learn from tickets and calls with `subject: { "kind": "thing", "name": "Bloom Salons" }` |
| An agent's own memory across runs | `{ "agents/writer": "write" }` | It reads its space at the start and writes what it learned at the end |
| An analyst across every customer | `{ "customers/*": "read" }` | `search` with no `spaces` searches all of them |

When the user and the team both set a rule for the same thing, the user's own wins for them: Maya gets casual, everyone else in the team gets formal.

With `append`, a teammate's different statement is added beside the original, with their name, instead of overwriting it. Rahul's token has `append` on `teams/acme`:

```json
POST /notes { "space": "teams/acme", "text": "Acme launches Product X on December 1." }
201 { "outcome": "added", "note": { "path": "facts/acme-launches-product-x-on-december-1-4eca", "written_by": "users/rahul", … } }
```

The team space now has Maya's "November 3" and Rahul's "December 1", each with who said it. A `DELETE` from Rahul there answers `403`.

### Notes many agents edit

For a document several agents maintain (product truth, a plan, a playbook), write it like a file and edit single lines:

```bash
curl -X PUT $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "product-truth", "path": "pricing/plans", "title": "Plans", "kind": "doc",
        "body": "# Plans\n\nStarter is $10 a month for 3 seats.\nBusiness is $49 a month for 20 seats." }'
# 201 { "created": true, "note": { "path": "pricing/plans", "version": 1, … } }

curl -X PATCH $MEM/notes -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{ "space": "product-truth", "path": "pricing/plans", "if_version": 1,
        "ops": [{ "op": "str_replace", "old": "Starter is $10", "new": "Starter is $12" }, { "op": "append", "text": "Enterprise: ask sales." }] }'
# 200 { "note": { "path": "pricing/plans", "version": 2, "body": "# Plans\n\nStarter is $12 a month…\nEnterprise: ask sales.", … } }
```

`if_version` is the version the agent read. If someone changed the note since, the write is refused with `409` ("is at version 2, not 1: read it again before writing"), so two agents never silently overwrite each other. Edit ops: `str_replace`, `insert` (after a line number), `append`, `prepend`, `delete`.

### Look back in time

Every change keeps the old version. `GET /history?space=…&path=…` lists them with who changed what and when. Add `as_of` to `index`, `open`, `search` or `entity` to see memory as it was:

```bash
curl "$MEM/open?space=product-truth&path=pricing/plans&as_of=2026-10-22T09:34:35Z" -H "Authorization: Bearer $TOKEN"
# { "note": { "version": 1, "body": "# Plans\n\nStarter is $10 a month for 3 seats.\n…", … } }
```

### Everything about one name

```bash
curl "$MEM/entity/acme?spaces=teams/acme" -H "Authorization: Bearer $TOKEN"
```

```json
{ "entity": { "name": "acme", "type": "organisation",
  "notes": [ { "summary": "Acme uses HubSpot as its CRM.", … }, { "summary": "Acme launches Product X on November 3.", … }, … ],
  "links": [
    { "subject": "acme", "relation": "uses", "object": "hubspot", … },
    { "subject": "acme", "relation": "launches", "object": "product x", … },
    { "subject": "acme", "relation": "moved_to", "object": "enterprise plan", … } ],
  "connected": [ { "name": "hubspot", "links": 1 }, { "name": "product x", "links": 1 }, … ] } }
```

Good for an account brief. `GET /entities?space=teams/acme` lists every name a space knows. Recall uses these links too, so a question about Acme also brings up what Acme is connected to.

### Memory as tools for viaSocket's AI

To let viaSocket's AI decide for itself when to look something up or save it, give it memory endpoints as `tools` on `user.ai.send`. Tool headers pass through viaSocket's AI service, so use a separate short-lived memory token for them:

```js
import { newMemoryToken } from './memory.js'

export async function memoryTools(userId) {
  const { token, url, me, spaces } = await newMemoryToken(userId, 900) // 15 minutes, for one AI call
  const headers = { Authorization: `Bearer ${token}` }
  const writable = Object.keys(spaces).filter((space) => spaces[space] !== 'read' && !space.endsWith('/*'))
  return [
    { name: 'search_memory', method: 'POST', url: `${url}/search`, headers,
      description: 'Look up what is remembered: rules, facts, lessons and how named things connect. Use it when the answer may depend on something said or decided before.',
      fields: { query: { type: 'string', description: 'What to look up, in a few words or a sentence' } },
      requiredParams: ['query'], toolAndVariablePath: {} },
    { name: 'lookup_entity', method: 'POST', url: `${url}/entity`, headers,
      description: 'Everything remembered about one named customer, company, product, app or person, and the names it connects to.',
      fields: { name: { type: 'string', description: 'The name, lowercase, such as "acme"' } },
      requiredParams: ['name'], toolAndVariablePath: {} },
    { name: 'list_memory', method: 'GET', url: `${url}/index`, headers,
      description: 'List the notes in a space or folder, with one-line summaries. With since, only what changed after that date-time.',
      fields: { space: { type: 'string', description: `One of: ${Object.keys(spaces).join(', ')}` },
                path: { type: 'string', description: 'A folder in the space (optional)' },
                since: { type: 'string', description: 'ISO date-time (optional)' } },
      requiredParams: ['space'], toolAndVariablePath: {} },
    { name: 'open_note', method: 'GET', url: `${url}/open`, headers,
      description: 'Read one note in full.',
      fields: { space: { type: 'string', description: 'The space' }, path: { type: 'string', description: 'The note path from list_memory' } },
      requiredParams: ['space', 'path'], toolAndVariablePath: {} },
    { name: 'remember', method: 'POST', url: `${url}/notes`, headers,
      description: 'Save one lasting fact, merged with what is known (a repeat confirms, a change replaces). This writes to memory. Never save secrets or one-off tasks.',
      fields: { text: { type: 'string', description: 'One short sentence that makes sense later on its own' },
                space: { type: 'string', description: `Where to save, one of: ${writable.join(', ')}. The user's own space is ${me}` } },
      requiredParams: ['text', 'space'], toolAndVariablePath: {} },
  ]
}

// await userOf(userId).ai.send({ threadId, message, prompt, tools: await memoryTools(userId) })
```

These are written by hand because viaSocket's AI takes simple fields, while `GET /tools` describes the full calls (nested arrays and objects) for models you call yourself. Tell the AI in your `prompt` when to use them ("Search memory before answering questions about customers; remember decisions the user states"). Leave out `remember` for an AI that should only read.

### An agent team around a shared hub

Many agents (an SEO analyst, a writer, a prospector, a support agent, and so on) sharing what they know through a few common spaces, with people deciding. Every piece is above; this is how they fit.

**Spaces:**

| Space | Holds | Written by | Read by |
| --- | --- | --- | --- |
| `product-truth` | What the product does today; every claim is checked against it | Its keeper agent and people (`write`) | Every agent |
| `brand-voice` | Rules: tone, words to use and avoid, per channel (`kind: "rule"`, one `slot` each) | People (`write`) | Agents that write |
| `decisions` | What was decided; agents' proposals under `decisions/proposed/` | Agents `append` proposals; people `write` | Every agent |
| `customers/<domain>` | One memory per account | Support, success and sales agents | Agents that need the account |
| `intelligence` | Market and competitor findings | The watcher and tracker agents (`append`) | Every agent |
| `findings/<process>` | What a process learned (`findings/sales`, `findings/content`) | That process's agents (`append`) | Every agent, the hub keeper, people |
| the agent's own `me` | Its notes across runs and its lessons from feedback | The agent | The agent |

**Each agent is one identity** with its own embed token. Sign it with a stable id and the spaces it needs:

```js
// memory.js: each agent's grants, by the id it is signed as
const AGENT_SPACES = {
  'agent-content-planner': {
    'product-truth': 'read', 'brand-voice': 'read', 'intelligence': 'read', 'findings/*': 'read',
    'decisions': 'append', 'findings/content': 'append',
  },
  // 'agent-search-analyst': { … },
}
export const memorySpacesOf = (userId) => AGENT_SPACES[userId]
```

Each agent also has its own `me` for its notes across runs. People get their own tokens the same way, with `write` on the hub spaces they own. Folder grants (`customers/*`, `findings/*`) keep each token within the 20-space limit.

**Each run of an agent:**

1. **Catch up**: `GET /index?space=findings/sales&since=<its last run>` (and the other spaces it reads) lists only what changed since.
2. **Recall for the task**: `POST /search` with the task over `[me, "brand-voice", "product-truth", ...]`; put `prompt` into the agent's system prompt. Brand-voice rules come first in it.
3. **Work**, with memory tools if the agent decides for itself what to look up.
4. **Write back**: findings with `POST /notes` or `POST /learn` into its findings space, with `source: { "type": "run", "id": "<run id>" }`; corrections to shared notes with `PATCH /notes` and `if_version`; its own notes in `me`.
5. **Propose, don't decide**: anything that needs a person goes to `decisions/proposed/<topic>` (`PUT /notes`). A person accepts by writing it into `decisions/` and deleting the proposal, or rejects by archiving it.

**People stay in charge**: `GET /index?space=decisions&path=proposed` is their review queue, `GET /history` shows which agent changed what, and `POST /feedback` on an agent's work becomes a lesson in that agent's space for next time. Deleting a run's `source` removes everything a bad run wrote.

### MCP and tools for agents

To let an agent decide for itself when to look things up and what to save, give it memory as tools. Any MCP client (Claude Desktop, Claude Code, Cursor, agent frameworks) can use memory directly. Point it at `mcp_url` with the memory token:

```json
{ "mcpServers": { "memory": { "url": "https://memory-api.viasocket.com/mcp", "headers": { "Authorization": "Bearer <memory token>" } } } }
```

```bash
claude mcp add --transport http memory https://memory-api.viasocket.com/mcp --header "Authorization: Bearer <memory token>"
```

The agent gets ten tools, and only the spaces its token grants: `memory_spaces`, `memory_index`, `memory_open`, `memory_search`, `memory_entity`, `memory_write`, `memory_edit`, `memory_remember`, `memory_learn`, `memory_delete`. Tokens expire: an expired one gets `401` on every call. For an agent your server runs, get a fresh token (`newMemoryToken`) for each session. A token pasted into Claude Desktop or Cursor lasts at most 24 hours (`newMemoryToken(userId, 86400)`), then needs replacing.

For a model you call yourself, `GET $MEM/tools` returns the same ten tools as function definitions, each with the method and URL to call.

## Reference

Base URL `https://memory-api.viasocket.com`. Every call takes `Authorization: Bearer <memory token>`, sends and answers JSON, and works on the spaces the token grants. GET and DELETE take their fields in the query string or the body; in a query string, `spaces`, `labels` and `kinds` are comma separated (`spaces=users/18231,teams/acme`) and URL-encoded. The service has no rate limit of its own; `/learn` and writes that call the model take seconds, so don't run hundreds at once.

### Shapes used below

**Note**

```json
{
  "space": "teams/acme",
  "path": "facts/acme-uses-hubspot-as-its-crm-a314",
  "kind": "fact",
  "slot": null,
  "title": null,
  "summary": "Acme uses HubSpot as its CRM.",
  "labels": [],
  "version": 1,
  "status": "active",
  "valid_from": "2026-10-22T09:34:55.093Z",
  "valid_to": null,
  "source": { "type": "thread", "id": "chat-8f2a" },
  "written_by": "users/maya",
  "change": "learn",
  "confidence": 0.9,
  "confirmed": 1,
  "created_at": "2026-10-22T09:34:55.093Z",
  "updated_at": "2026-10-22T09:34:55.093Z",
  "body": "Acme uses HubSpot as its CRM."
}
```

| Field | Meaning |
| --- | --- |
| `kind` | `rule` (applied to every answer; has a `slot` such as `tone`), `fact`, `lesson` (from feedback), `doc` (a document), `note`, or your own word |
| `status` | `active`; `archived` (kept in history, no longer used); or `expired` (past its `valid_to`, listed only with `all=true`) |
| `valid_to` | After this moment the note is no longer used |
| `change` | What made this version: `write`, `edit`, `edit-metadata`, `remember`, `learn`, `feedback`, `document` |
| `confidence` | How sure extraction was (0 to 1), or `null` for notes you wrote |
| `confirmed` | How many times the same thing was said again |
| `body` | Only in replies about one note (open, write, edit, remember, learn); lists leave it out |

A rule saved without a `slot` is still applied on every answer, but nothing replaces it: give rules a slot. Names in the graph are lowercase; send them URL-encoded in a path (`/entity/bloom%20salons`). `GET /index` returns at most `limit` notes and no total: when a page comes back full, ask for the next with `offset`.

**Link** (between two names): `{ "subject": "acme", "relation": "uses", "object": "hubspot", "space": "teams/acme", "path": "facts/…", "as_of": "<date>", "until": null }`

**Document**: `{ "url": "https://…" | null, "filename": null, "mime": null, "bytes": 156, "status": "queued" | "loading" | "indexing" | "indexed" | "error", "error": null | "<why>", "extracted_at": "<date>" | null }`

### Read

| Call | Fields | Reply |
| --- | --- | --- |
| `GET /spaces` | | `{ "spaces": [{ "name", "access": "read" \| "append" \| "write", "notes": 12, "updated_at" }] }` |
| `GET /index` | `space` or `spaces` (comma separated), `path` (a folder), `kind`, `labels`, `as_of`, `since` (only notes changed after this date-time, newest first), `all=true` (with archived), `limit` (100, at most 500), `offset` | `{ "spaces": ["teams/acme"], "path": null, "folders": ["facts"], "notes": [Note without body] }` |
| `GET /open` | `space`, `path`, `version`, `as_of` | `{ "note": Note + "document": Document \| null }` |
| `GET /history` | `space`, `path` | `{ "space", "path", "versions": [{ "version", "title", "summary", "written_by", "change", "created_at", "body" }] }` |
| `POST /search` (or `GET /search`) | `query`; `spaces` (in order of precedence; default every space the token reads); `path` (only notes under this folder; rules and lessons still come from the whole space); `top_k` (5, at most 50); `kinds`; `labels`; `as_of`; `prompt: false` (plain search); `max_distance` (0.66) | `{ "prompt": "<block>" \| null, "rules": [Note], "results": [Note + "excerpt", "chunk", "distance", "score"], "connections": [{ "name", "links": [Link], "phrases": ["→ uses hubspot"] }], "lessons": [Note + "excerpt", "distance", "score"] }` |
| `GET /entity/:name` (or `POST /entity` with `name`) | `spaces`, `as_of` | `{ "entity": { "name", "type", "notes": [Note], "links": [Link], "connected": [{ "name", "links": 2 }] } }` |
| `GET /entities` | `space`, `q` (start of a name) | `{ "entities": [{ "name", "type", "notes": 3 }] }` |

### Write

| Call | Fields | Reply |
| --- | --- | --- |
| `POST /learn` | `space`; `messages` (`[{ "role", "content" }]`, up to 200) or `text`; `shared_space`; `share` (`infer` \| `all`); `subject` (`{ "kind": "person" \| "thing", "name" }`); `instructions`; `source`; `rules_allowed: false` (learn facts only, for text that is not the user's own words) | `{ "queued": false, "changes": [{ "action": "added" \| "updated" \| "confirmed" \| "replaced" \| "forgotten", "note": Note, "previous"?: { "path", "text" } }], "skipped": 0, "failed": 0 }` |
| `POST /notes` | `space`, `text`; `kind`, `slot`, `labels`, `valid_to`, `source`, `path`, `confidence`; `merge: false` (save as sent); `guard: false` (allow private data); `entities` and `relations` (see below) | `{ "outcome": "added" \| "confirmed" \| "replaced", "note": Note, "previous"?: { "path", "text" }, "indexed": "indexed" \| "queued" \| "failed" }` (201 when added) |
| `PUT /notes` | `space`, `path`, `body`; `title`, `kind`, `slot`, `labels`, `valid_from`, `valid_to`, `source`, `confidence`, `if_version`; `entities` and `relations` (see below) | `{ "created": true \| false, "note": Note, "indexed" }` (201 when created) |
| `PATCH /notes` | `space`, `path`; `ops`: `{ "op": "str_replace", "old", "new" }`, `{ "op": "insert", "line", "text" }`, `{ "op": "append" \| "prepend", "text" }`, `{ "op": "delete", "old" }`, or `body` to replace the whole text; `title`, `labels`, `kind`, `slot`, `valid_to`; `if_version` | `{ "note": Note, "indexed" }` |
| `POST /documents` | `space`; `url` or `content`; `path`, `title`, `labels`, `source` | `202 { "note": Note, "document": Document }` |
| `POST /feedback` | `space`, `task`, `output`, `outcome` (`good` \| `bad` \| `corrected`); `correction`, `note` | `{ "changes": [{ "action": "added" \| "reinforced" \| "revised", "note": Note, "previous"?: { "path", "text" } }] }` |

### Forget

| Call | Fields | Reply |
| --- | --- | --- |
| `DELETE /notes` | `space`, `path`; `archive: true` to keep it in history | `{ "deleted": 1 }`, or `{ "archived": true, "note": Note }` |
| `DELETE /source` | `type`, `id`; `spaces` (default every space the token may write) | `{ "source": { "type", "id" }, "deleted": 3 }` |
| `DELETE /entity/:name` | `spaces` | `{ "name", "deleted": 2 }` |
| `DELETE /spaces` | `space` | `{ "space", "deleted": 12 }` |

Deleting needs `write` on the space. Writing needs `append` or `write`; with `append`, a note someone else wrote is never replaced or deleted.

`entities` and `relations` on a write: when your code already knows the names in a note and how they link, send them (`"entities": [{ "name": "acme", "type": "organisation" }]`, `"relations": [{ "subject": "acme", "relation": "uses", "object": "hubspot" }]`) and the note is linked without a model call. Otherwise a model finds them.

### Service

| Call | Reply |
| --- | --- |
| `GET /health` (no token) | `{ "status": "ok", "database": true, "embedding_model", "extraction_model", "queue": false }`; `503` while the database is unavailable |
| `GET /guide.md` (no token) | This guide as Markdown |

### For a model

| Call | Reply |
| --- | --- |
| `GET /tools` | `{ "tools": [{ "type": "function", "function": { "name", "description", "parameters" }, "http": { "method", "url" } }] }`: the MCP tools as function definitions |
| `POST /mcp` | MCP over streamable HTTP (stateless, POST only) |

| MCP tool | Arguments | Same as |
| --- | --- | --- |
| `memory_spaces` | | `GET /spaces` |
| `memory_index` | `space`, `path`, `as_of`, `since`, `kind`, `labels`, `all`, `limit`, `offset` | `GET /index` |
| `memory_open` | `space`, `path`, `as_of`, `version` | `GET /open` |
| `memory_search` | `query`, `spaces`, `path`, `kinds`, `labels`, `as_of`, `top_k`, `prompt` | `POST /search` |
| `memory_entity` | `name`, `spaces`, `as_of` | `POST /entity` |
| `memory_write` | `space`, `path`, `body`, `title`, `kind`, `slot`, `labels`, `valid_to`, `source`, `if_version` | `PUT /notes` |
| `memory_edit` | `space`, `path`, `ops`, `if_version` | `PATCH /notes` |
| `memory_remember` | `space`, `text`, `kind`, `slot`, `labels`, `valid_to`, `source`, `merge`, `path` | `POST /notes` |
| `memory_learn` | `space`, `messages` or `text`, `shared_space`, `share`, `subject`, `instructions`, `source`, `rules_allowed` | `POST /learn` |
| `memory_delete` | `space` + `path` (with `archive: true` to keep history), or `name`, or `source` as `{ "type", "id" }` (with `spaces`) | `DELETE /notes`, `/entity`, `/source` |

History, documents and feedback are HTTP only. Each tool answers with its HTTP reply as JSON text.

**Not using viaSocket Embed?** Ask viaSocket for a signer of your own. Your backend then signs memory tokens itself, with HS256 and your secret: `{ "iss": "<your signer>", "sub": "<caller>", "org": "<your org id>", "project": "<your project id>", "spaces": { "users/maya": "write", "teams/acme": "append" }, "exp": … }`. Everything else in this guide works the same.

## Recipes

**A chat assistant that remembers users**: a memory token per user; `POST /search` with the user's message, add `prompt` to your system prompt, answer, then `POST /learn` with the exchange and `source` set to your thread id (Level 1). Add a "Your memory" page with list and delete (Level 2), and `DELETE /source` when a thread is deleted.

**A support agent that gets better**: recall from `agents/support` and `product-truth` before each ticket, send corrections to feedback with `space: "agents/support"`, and load help articles with `POST /documents`.

**Customer account memory**: one space per customer, `customers/<domain>`. Learn from calls, emails and tickets with `subject: { "kind": "thing", "name": "<customer>" }` and a `source` for each. `GET /entity/<customer>` gives the account brief; `as_of` shows what was known at renewal time.

**An agent team with a shared hub**: see [An agent team around a shared hub](#an-agent-team-around-a-shared-hub).

**Several agents, one source of truth**: a space `product-truth`, `write` for the agents that maintain it and `read` for the rest. Each agent lists the space first, opens what it needs, and edits single lines with `if_version`, re-reading on `409`.

## Behaviour worth knowing

- **Language.** Search by meaning is multilingual: a question in Hindi or Hinglish finds a memory written in English, and the other way round.
- **Distance.** Each result carries `distance`: 0 is the same text. Direct questions find their memory at about 0.2 to 0.63, off-topic questions sit beyond 0.7, and search returns notes up to 0.66 (a little further when they share a word or a name with the question), so an unrelated question gets nothing back. A question that only implies the memory ("my pet's name" for "has a dog named Bruno") can land past the cut-off: name the thing, or send a larger `max_distance` for a looser search.
- **Connections are named, not inferred.** "Order lunch for the team" does not find "is vegetarian" unless the words or names overlap or the meaning is close. Search for what the task needs ("food preferences") or keep such facts in a folder you recall for that task.
- **Writes answer at once; long ones finish after the reply.** A note longer than about 4,000 characters answers with `indexed: "queued"` and becomes searchable a few seconds later. **Learning takes 1 to 6 seconds** and answers when it is done. Call it after you have replied to the user, or without waiting for it.
- **A note's path is its identity.** It stays the same when the note changes, so `facts/favourite-database-is-mongodb-…` can hold "Favourite database is PostgreSQL" after an update. Read `summary` or `body`, never the path, for what a note says.
- **Merging is by meaning.** A merging save (`POST /notes`, `POST /learn`) treats two statements as one when they say the same thing; ids, numbers and domains keep records apart. For records that must never merge, use `PUT /notes` at your own path.
- **Rules apply everywhere.** Every rule in the searched spaces is in every recall. A rule for one situation ("detailed for code reviews") gets its own slot beside the general one.
- **Expired notes** (past `valid_to`) are left out of search and `/index`; `/index?all=true` lists them with `status: "expired"`.
- **Send a `User-Agent` header** from server code. Python's default (`Python-urllib`) is blocked by the firewall in front of the service; Node's `fetch` works, and so does the `viasocket-apps` package, which uses it.

## Errors and limits

Errors are JSON with a status that says what went wrong:

```json
{ "status": "error", "success": false, "message": "This token cannot read the memory space \"teams/globex\"", "data": null }
```

| Status | Means | Fix |
| --- | --- | --- |
| `400` | A field is missing or wrong, or private data was refused ("memory never keeps passwords, keys, email addresses…") | Read `message`; send `guard: false` to save personal data on purpose |
| `401` | No token, a bad one, or expired. From the memory token endpoint: the embed token's `memory_spaces` is not valid | Get a new memory token; fix the claim |
| `403` | The token doesn't grant that space, or not at that level | Grant it in `memory_spaces` |
| `404` | Nothing at that path or name | |
| `409` | Either someone changed the note since you read it ("is at version 3, not 2"), or the text to edit appears more than once or not at all | First case: open the note again, redo the edit on the new text, retry. Second case: quote more of the text, or the exact text; retrying the same edit fails again |
| `413` | The request is over 16 MB | Send less, or split a document |
| `500`, `502`, `503`, `504` | The service or something it depends on failed. From the memory token endpoint, `503` means memory is not switched on for that viaSocket server | Retry with backoff (1 s, 4 s, 15 s). Recall failing should never stop your AI from answering |

Limits: 20 shared spaces per embed token; 200 messages and 20,000 characters per message in `learn`; notes up to 2,000,000 characters; 10 labels per note; documents up to 20 MB; memory tokens up to 24 hours from viaSocket.

## Pricing

You pay per successful call, with no monthly fee and no minimum. Failed calls, deletes and listing the tools are free.

| Call | Price per 1,000 calls |
| --- | --- |
| Read: search, recall, list, open, history, entities (and the same MCP tools) | $0.002 |
| Write or edit a note (`PUT /notes`, `PATCH /notes`, `memory_write`, `memory_edit`) | $0.10 |
| Remember one thing (`POST /notes`, `memory_remember`) | $0.30 |
| Feedback (`POST /feedback`) | $0.30 |
| Learn from a conversation (`POST /learn`, `memory_learn`) | $1.00 |
| Add a document (`POST /documents`) | $1.00 |

With viaSocket, usage is charged to your viaSocket organisation once an hour. A product that recalls before every answer and learns after it pays about $1.00 per 1,000 messages.

## How it compares to mem0, Supermemory, Letta, Zep and past.dev

From each product's own docs, 22 Oct 2026:

| | Stores | Adding memory | Scoped by | Old versions | Documents | MCP |
| --- | --- | --- | --- | --- | --- | --- |
| **mem0** | Facts extracted from messages, with entities | `add` (LLM extracts) | `user_id`, `agent_id`, `app_id`, `run_id` | history per memory | images, PDFs | yes |
| **Supermemory** | Documents and facts extracted from them | add a document (async) | namespace / container tag | latest flag and parent | text, URLs, PDF, DOCX, images, video | yes (hosted) |
| **Letta** | An agent's own memory files | the agent's tools | per agent | git history | not confirmed | manages agents |
| **Zep** | A temporal graph of facts and entities | messages, episodes | user, thread, graph | facts invalidated | grouped by document | yes |
| **past.dev** | Dated text; recall returns dated evidence | ingest (async) | project, audiences | previous values | emails, transcripts, documents | yes |
| **viasocket-memory** | Notes in spaces: facts, rules, lessons and whole documents | `learn` (LLM extracts), `remember`, or write it yourself | spaces, per token as read, append or write | every version; `as_of` on every read | web pages, PDFs, Google Docs, YouTube, text | yes |

If you know mem0: `add(messages)` is `POST /learn`, `add(infer=False)` is `POST /notes` with `merge: false`, `search` is `POST /search`, `get_all` is `GET /index`, `history` is `GET /history`, `update` is `PATCH /notes`, and `delete` and `delete_all` are `DELETE /notes` and `DELETE /spaces`.

What is different here: memories are files you can also write and edit by hand, beside the ones learned from chat; who sees what is decided by the token, for every call including MCP; rules, facts and lessons are kept apart and used differently; and recall hands back a ready prompt block instead of a list you still have to format.
