# Memsy > Persistent memory for AI agents and applications — official SDKs for Python and Node.js, plus the raw HTTP API. This document contains the full content of all documentation pages for AI consumption. --- ## Actors and Sessions **URL:** https://docs.memsy.io/docs/actors-and-sessions **Description:** How actor_id and session_id control grouping and retrieval. Events carry two identifiers that control how Memsy groups and retrieves memories: `actor_id` and `session_id`. Getting these right upfront makes search meaningfully better later. ## `actor_id` — *who* The actor is the entity the memory is **about**. In most applications this is the end user. It can also be an AI agent, a customer account, or any stable identity you want to attach memories to. - Pick an ID that is stable across sessions (your internal user UUID), not something that rotates (like a JWT). - Use the same `actor_id` across all event kinds for a given person — `user_message`, `assistant_message`, `tool_result` all share the actor. - Scope a search to one actor: ## `session_id` — *where in the conversation* The session is the immediate context window the event belongs to — a conversation, a thread, a job run. - Start a new `session_id` when the context logically resets (new chat, new task). - Keep the same `session_id` for every event that belongs to one conversational flow, including assistant replies and tool results. Sessions aren't used to filter `search()` today, but Memsy uses them during extraction to understand what's in scope. ## A complete example ## Shared actor IDs vs. isolation Memories are always isolated per organization (each API key belongs to one org). Within an org, memories are keyed by `actor_id`. If two users share an `actor_id`, they share memories — so don't reuse IDs across people. ## Next - How `ingest()` returns before processing completes → **[Async Processing](/docs/async-processing)**. --- ## Async Client (Python) **URL:** https://docs.memsy.io/docs/async-client **Description:** AsyncMemsyClient mirrors the Python sync client method-for-method. `AsyncMemsyClient` mirrors the Python `MemsyClient` method-for-method. If you're building Python on `asyncio` — FastAPI, agent runtimes, worker pools — use it. ## Setup ```python from memsy import AsyncMemsyClient, EventPayload async def main(): async with AsyncMemsyClient( base_url=os.environ["MEMSY_BASE_URL"], api_key=os.environ["MEMSY_API_KEY"], ) as client: health = await client.health() print(health.status) asyncio.run(main()) ``` The `async with` block closes the underlying connection pool for you. If you can't use a context manager, call `await client.close()` explicitly. ## Every method is `async` The interface is identical to the sync client — just add `await`: ```python await client.ingest([...]) await client.search("query", actor_id="user_42") await client.status(event_ids=[...]) await client.health() await client.clear("container_tag") # currently a 501 stub — see note below ``` All parameter names, defaults, return types, and exceptions match the sync client. ## Parallel calls ```python async def personalise(user_id: str): async with AsyncMemsyClient(...) as client: prefs, recent = await asyncio.gather( client.search("preferences", actor_id=user_id, limit=5), client.search("recent topics", actor_id=user_id, limit=5), ) return prefs.results, recent.results ``` `httpx.AsyncClient` (the underlying transport) pools connections per-client, so repeated calls within one `AsyncMemsyClient` share TCP connections. ## Long-lived clients in web servers In a FastAPI or Starlette app, create one `AsyncMemsyClient` per process and reuse it — don't create one per request. ```python from contextlib import asynccontextmanager from fastapi import FastAPI from memsy import AsyncMemsyClient memsy: AsyncMemsyClient | None = None @asynccontextmanager async def lifespan(app: FastAPI): global memsy memsy = AsyncMemsyClient( base_url=os.environ["MEMSY_BASE_URL"], api_key=os.environ["MEMSY_API_KEY"], ) yield await memsy.close() app = FastAPI(lifespan=lifespan) @app.post("/chat") async def chat(msg: str): results = await memsy.search(msg, actor_id="user_42") ... ``` ## Parity guarantee Every public method on `MemsyClient` also exists on `AsyncMemsyClient` with the same signature. When a new method is added to one, the other follows in the same release. --- ## Async Processing **URL:** https://docs.memsy.io/docs/async-processing **Description:** Why ingest returns before processing completes — and how to poll. Memsy intentionally decouples ingestion from processing. This matters in a few places, so it's worth understanding the model. ## What happens when you call `ingest()` The server: 1. Validates the payload. 2. Writes the raw events to durable storage. 3. Returns a list of `event_ids`. 4. **Enqueues** the events for downstream processing (LLM extraction → embedding → indexing). Steps 1–3 happen synchronously over HTTP. Step 4 happens on the server's worker pool, after your call has already returned. ## Why it's designed this way - Applications in the hot path (chat loops, agent runtimes) log events without blocking on LLM latency. - Extraction can batch, retry, and prioritize without the client caring. - Costs and failures on the extraction side don't punish the caller. ## When can I search for a newly ingested event? Usually within a few seconds, but there's no guarantee. If your UX depends on memory being present immediately after ingest, poll for completion: The status response partitions the IDs you sent into three buckets (Python / Node): - `completed_ids` / `completedIds` — extracted, embedded, indexed. Searchable now. - `pending_ids` / `pendingIds` — still in the queue or mid-processing. - `failed_ids` / `failedIds` — something went wrong during extraction. These won't become memories. ## What causes an event to fail? - Content rejected by a content filter. - An extraction-time LLM call that could not be recovered after retries. - An event that referenced a deleted or quarantined resource. Failures are rare but not zero. Check `failed_ids` in your pollers if correctness matters. ## Design implications - **Re-ingesting an event is safe within a 60-second window** — Memsy de-duplicates by `(actor_id, session_id, kind, content)` and returns the original `event_id` so `status()` keeps working. Outside the window, identical payloads will create separate events; use `status()` to confirm processing instead of re-sending. - **Do batch aggressively.** `ingest()` accepts many events per call; fifty events in one batch is much cheaper than fifty single-event calls. ## Next - How to build batches that ingest well → **[Ingesting events](/docs/ingesting-events)**. --- ## Console Memories **URL:** https://docs.memsy.io/docs/console-memories **Description:** Browse and inspect stored memories via client.memories. `client.memories` exposes read-only access to the memories stored in your org. Use it to audit extraction quality, debug unexpected search results, or build admin dashboards. ## List memories ### Pagination Pagination is stable across pages — the server always sorts by your chosen sort key with `memory_id` as a secondary tiebreaker, so iterating `offset` won't return duplicates or skip records even when many memories share the primary sort value (e.g. identical `observed_at`). ## Get a single memory ## Stats Aggregate statistics for all memories in the org: --- ## `MemoryItemResource` fields | Field | Type | Description | |---|---|---| | `memory_id` | `str` | Unique memory ID. | | `org_id` | `str` | Owning org. | | `scope` | `MemoryScopeInfo` | Scope (level + actor/team/role IDs). | | `type` | `str` | Memory type (e.g. `"preference"`, `"fact"`). | | `kind` | `str` | Memory kind (e.g. `"semantic"`, `"episodic"`). | | `memory_kind` | `str` | Low-level memory kind classification. | | `status` | `str` | `"active"`, `"archived"`, or `"decayed"`. | | `text` | `str` | The extracted memory text. | | `confidence` | `float` | Extraction confidence (0–1). | | `strength` | `float` | Reinforcement strength (0–5.0, default 1.0). Not a probability. | | `recall_count` | `int` | Number of times this memory was recalled in search. | | `decay_half_life_days` | `float` | Days until strength halves. | | `pinned` | `bool` | Pinned memories never decay. | | `tags` | `list[str]` | Tags from extraction. | | `entity_refs` | `list[dict]` | Named entity references. | | `source_event_ids` | `list[str]` | IDs of contributing events. | | `source_urls` | `list[str]` | Source URLs if applicable. | | `summary` | `str \| None` | Short auto-generated summary. | | `payload` | `dict \| None` | Raw extraction payload from the LLM, if retained. | | `last_recalled_at` | `str \| None` | ISO-8601 timestamp of last search recall. | | `observed_at` | `str \| None` | ISO-8601 timestamp of first observation. | | `effective_from` | `str \| None` | Start of validity window. | | `effective_to` | `str \| None` | End of validity window (`None` = indefinite). | | `created_at` | `str \| None` | Record creation timestamp. | | `updated_at` | `str \| None` | Last update timestamp. | ### `MemoryScopeInfo` ```python @dataclass class MemoryScopeInfo: level: str # "org" | "role" | "team" | "actor" actor_id: str | None = None team_id: str | None = None role_id: str | None = None ``` --- ## Async usage ```python from memsy import AsyncMemsyClient async with AsyncMemsyClient(base_url="...", api_key="msy_...") as client: stats = await client.memories.stats() result = await client.memories.list(kind="semantic", limit=20) memory = await client.memories.get("mem_01abc...") ``` --- ## See also - **[Onboarding](/docs/onboarding)** — set up org/role/team hierarchy to scope memories. - **[Searching memory](/docs/searching-memory)** — retrieve memories via semantic search. - **[Models](/docs/reference/python/models)** — full `MemoryItemResource` and `MemoryStatsResponse` definitions. --- ## Error Handling **URL:** https://docs.memsy.io/docs/error-handling **Description:** The typed exception hierarchy and how to catch each case. All SDK calls throw/raise instances of a single exception tree. Catching the right one lets you distinguish "need to reconnect" from "upgrade your plan" from "bad credentials". Both SDKs expose the **same fine-grained hierarchy** mapped from the API's `error_code`. Python uses bare class names (`AuthorizationError`); Node prefixes everything with `Memsy` (`MemsyAuthorizationError`). ## The hierarchy ## A full example ## Control-plane errors (Python only) `MemsyControlClient` is currently Python-only. When using it, you may also encounter: ```python from memsy.exceptions import ( OrgLimitReachedError, KeyLimitReachedError, BillingNotEnabledError, SeatRequiredError, SeatLimitReachedError, ) try: key = control.keys.create("my-key", scopes=["read", "write"]) except KeyLimitReachedError as e: print(f"Key limit reached: {e.current}/{e.limit}") except BillingNotEnabledError as e: print(f"Billing not enabled. Express interest at: {e.interest_path}") ``` ## Exception-specific attributes (Python) ### `AuthorizationError` | Attribute | Description | |---|---| | `required_scope` | Scope your API key is missing, if known. | ### `FeatureNotAvailable` | Attribute | Description | |---|---| | `feature` | The gated feature's identifier. | | `current_tier` | The tier of the API key that tried to use it. | | `upgrade_url` | Deep-link to the dashboard billing page. | ### `OrgLimitReachedError` / `KeyLimitReachedError` | Attribute | Description | |---|---| | `limit` | Tier limit (orgs or keys). | | `current` | Current count. | ### `BillingNotEnabledError` | Attribute | Description | |---|---| | `interest_path` | API path to express Pro plan interest. | ### `SeatLimitReachedError` | Attribute | Description | |---|---| | `purchased_seats` | Total purchased seats. | | `assigned_seats` | Currently assigned seats. | | `pending_invites` | Pending invite count. | ### `RateLimitExceeded` | Attribute | Description | |---|---| | `retry_after` | Seconds to wait before retrying, parsed from `Retry-After`. | ### `UsageLimitExceeded` | Attribute | Description | |---|---| | `dimension` | What quota was exhausted (e.g. `"events_ingested"`). | | `current` | Current usage count. | | `limit` | The limit that was exceeded. | | `upgrade_url` | Deep-link to upgrade. | ### `MemsyAPIError` (base class) Every API-side error carries: | Attribute | Description | |---|---| | `status_code` | HTTP status. | | `detail` | Server-provided message. | | `error_code` | Stable machine-readable code, e.g. `"feature_not_available"`. | | `response` | The raw `httpx.Response` for deeper inspection. | ## Catch broadly or narrowly? - **In request handlers / agent loops** — catch `MemsyError` and surface a friendly error. Log the specific subclass for debugging. - **In pipelines with budgets** — handle `UsageLimitExceeded` and `FeatureNotAvailable` distinctly so you can route to an alert channel and link the upgrade page. - **In retryable background jobs** — catch `MemsyConnectionError` and `RateLimitExceeded` separately from API-level errors; they're the only ones that "just work" on a later retry. - **In control-plane admin code** — handle `BillingNotEnabledError` and `SeatLimitReachedError` and link users to the appropriate dashboard flow. ## Next - How automatic 429 retry behaves before these exceptions fire → **[Retries](/docs/retries)**. --- ## Events and Memories **URL:** https://docs.memsy.io/docs/events-and-memories **Description:** The mental model — raw events in, extracted memories out. Memsy splits your data into two distinct things: **events** (raw input) and **memories** (extracted, searchable output). ## Events: the raw input An **event** is an immutable record of something that happened in your application. You send events to Memsy; you never edit them after the fact. In the SDKs an event is an `EventPayload`: ### Event kinds | Kind | When to use it | |---|---| | `user_message` | A message authored by the end user. | | `assistant_message` | A reply generated by your AI. | | `tool_result` | Output from a tool/function call your assistant made. | | `app_event` | Anything else — domain events, lifecycle events, custom signals. | ### When do events show up in search? Never, directly. Search always returns **memories**, not events. If you want the original source events alongside results, pass `include_source_events=True` (Python) / `includeSourceEvents: true` (Node) to `search()`. ## Memories: the extracted output A **memory** is something Memsy has distilled from one or more events — a stable fact about an actor or session, written in natural language, embedded into a vector space, and indexed for semantic retrieval. You don't create memories directly; Memsy creates them from your events. In the SDKs a memory surfaces as a `SearchResult`: ## Typical flow ``` your app Memsy ──────── ───── build EventPayload │ ▼ ingest(events) ─────────► queue + extract + embed + index │ ▼ memory store ▲ │ search("...") ◄───── rank + return ───┘ ``` Events are cheap to produce; memories are created lazily. That's why `ingest()` returns immediately while `search()` reads from the processed index. ## Next - How actors and sessions scope your data → **[Actors and Sessions](/docs/actors-and-sessions)**. - Why ingest is async → **[Async Processing](/docs/async-processing)**. --- ## FAQ **URL:** https://docs.memsy.io/docs/faq **Description:** Answers to questions we hear most often. --- ## Memsy Docs **URL:** https://docs.memsy.io/docs **Description:** Persistent memory for AI agents and applications. Official SDKs for Python and Node.js, plus the raw HTTP API. **Memsy** turns the raw events your AI app produces — user messages, assistant replies, tool results — into structured, searchable long-term memory. This site documents the **Memsy HTTP API** and the official client libraries. Code examples throughout the docs are tabbed for **Python**, **Node.js**, and **cURL** — pick the language you work in. ## What the SDKs do - Ship events to Memsy with a single `ingest()` call (batched, async on the server). - Retrieve semantically relevant memories with `search()`. - Surface usage, rate-limit, and plan information on every response. - Map HTTP errors to strongly-typed exceptions (`AuthenticationError`, `RateLimitExceeded`, `MemsyAuthError`, …). - Retry rate-limited requests automatically with exponential backoff. ## What they do not do The SDKs are thin HTTP clients. They do not run memory extraction, embeddings, or vector search locally — all processing happens on the Memsy platform. ## Links - Dashboard & sign in — [app.memsy.io](https://app.memsy.io) - Python package — [pypi.org/project/memsy](https://pypi.org/project/memsy/) - Node package — [npmjs.com/package/@memsy-io/memsy](https://www.npmjs.com/package/@memsy-io/memsy) - Source code — [github.com/memsy-io/memsy](https://github.com/memsy-io/memsy) Ready? Head to the **[Quickstart](/docs/quickstart)**. --- ## Ingesting Events **URL:** https://docs.memsy.io/docs/ingesting-events **Description:** Batch size, metadata, timestamps, scoping — everything beyond the quickstart. `ingest()` takes a list of events and returns their IDs. ## Role and team scoping This is what sets Memsy apart: every event can be scoped to a role and team in your [onboarding hierarchy](/docs/onboarding). Extracted memories are stored at the right level of your org — not just a flat blob per user — so searches respect organizational context automatically. The memory extractor uses the role's and team's `promotion_prompt` to bias extraction toward what's relevant for that context. Memories then flow up the hierarchy: actor → role → team → org. `role_id`/`roleId` and `team_id`/`teamId` are optional. Events without them are stored at the actor scope. --- ## The minimum viable event Four fields. Everything else is optional. ## Event kinds `kind` is a closed enum. The API returns `422 Unprocessable Entity` for any other value. | Value | Use for | |---|---| | `user_message` | Something the user said or typed. Most common. | | `assistant_message` | Something the assistant said back. Useful so the model can recall its own past statements. | | `tool_result` | The output of a tool/function call (search result, calculator answer, API response). | | `app_event` | App-level signals that aren't a chat turn — e.g. "user changed plan to Pro", "deployment finished". | If you have a generic CRM event or a webhook payload, `app_event` is almost always the right answer. The extractor still pulls facts and preferences from it, just without conversational framing. There are no `chat_turn`, `tool_call`, `system`, or `note` kinds. Mapping notes: - **OpenAI/Anthropic message role `user`** → `user_message` - **role `assistant`** → `assistant_message` - **role `tool`** (or tool/function response) → `tool_result` - **system messages** → don't ingest them; they're rarely worth storing as memory ## Timestamps If your events are live, omit `ts` and the server stamps them. If you're **backfilling** historical data, always pass an ISO-8601 timestamp: Backfilled events with explicit timestamps get the same treatment as live events during extraction, but the timestamp is used for recency weighting in search. ## Metadata `metadata` is a **JSON-serialised string** (not a dict/object). Use it for application-level fields you want back with search results — tags, source identifiers, custom flags: Memsy echoes the raw metadata string back in each `SearchResult` so you can deserialize it client-side. Extracted memories carry the metadata of the source event(s) they were derived from under `metadata.source_metadata` on each search result — up to 5 entries per memory. Each entry is `{event_id, metadata: }` when the original string parsed as a JSON object, or `{event_id, raw: }` otherwise. **You will not lose the metadata you sent**, even if it wasn't valid JSON: the server preserves the original under `raw` instead of rejecting the ingest. ## Batching `ingest()` is designed for batches. Send many events in one call whenever you can: Recommended batch size: tens to low hundreds. Very large batches (thousands) increase tail latency and risk hitting request-size limits. ## Return value `event_ids` / `eventIds` gives you the IDs the server assigned, in the **same order** as the input. Pair them up if you need to track processing per event: ## Idempotency - Memsy de-duplicates within a rolling **60-second window** by `(actor_id, session_id, kind, content)` hash. Identical re-posts within the window are collapsed and the response returns the **original** `event_id` for each duplicate, so SDK retries and `status()` lookups continue to work. - Dedup is best-effort — under platform degradation you may still see fresh `event_id`s for duplicate submissions. Treat the window as a safety net for transient retries, not a guarantee. - If you genuinely need to re-ingest after the window, change any of the four fields — typically by appending `ts` to the content or sending a fresh `session_id`. ## Validation rules `POST /ingest` returns `422` if any event fails validation: - `content` must contain at least one non-whitespace character and stay under **8000 characters**. - `actor_id` and `session_id` must be non-empty after trimming whitespace; both are capped at **256 characters**. - `role_id` and `team_id` are also capped at **256 characters** and collapse to `null` when only whitespace. - `metadata` is capped at **4096 characters**. - NUL bytes are silently stripped from all string fields before persistence. Invalid JSON in `metadata` is **not** a validation error: the original string is preserved as `{"raw": }` on the event and surfaces back under `source_metadata` on derived memories. ## Failure modes `ingest()` itself can fail with (Python / Node): - `AuthenticationError` / `MemsyAuthError` — bad API key. - `UsageLimitExceeded` / `MemsyAPIError` (status `403`) — you've hit a quota (events, tokens, storage). - `RateLimitExceeded` / `MemsyRateLimitError` — the SDK retried automatically, but you still exhausted the budget. - `MemsyConnectionError` — network or timeout. Individual events can also **fail during extraction** after a successful ingest. Those show up in `status().failed_ids` / `status().failedIds` later. See **[Async Processing](/docs/async-processing)**. ## Next - Retrieve what you stored → **[Searching memory](/docs/searching-memory)**. --- ## Installation **URL:** https://docs.memsy.io/docs/installation **Description:** Install the Memsy SDK for your language — Python or Node.js — or call the HTTP API directly. Memsy ships official clients for **Python** and **Node.js**. Both are thin, typed HTTP clients with built-in retries, usage tracking, and identical method names. If neither matches your stack, the **cURL** tab on every page shows the raw HTTP call so you can wire it up in any language. ## Requirements ## Install ## Verify the install ## Dev extras (Python only) If you're contributing to the Python SDK or want the test tooling locally: ```bash pip install "memsy[dev]" ``` Adds `pytest`, `pytest-asyncio`, `anyio`, `black`, and `ruff`. ## Next → **[Run your first call](/docs/quickstart)** --- ## Onboarding Hierarchy **URL:** https://docs.memsy.io/docs/onboarding **Description:** Create orgs, roles, and teams to scope memory extraction and retrieval. Memsy supports a three-level onboarding hierarchy — **org → role → team** — that controls how memories are scoped and how extraction prompts are customised. Setting this up is optional: the SDKs work fine without it, but adding hierarchy lets you tune extraction for different parts of your user base. The CRUD operations on this page are exposed in **both** SDKs via `client.orgs`, `client.roles`, and `client.teams`. Most of these endpoints are **admin-gated**; non-admin keys will receive a typed `AuthorizationError` (Python) / `MemsyAuthorizationError` (Node). ## Hierarchy overview ``` org ├── team (e.g. "platform team", "enterprise sales") └── role (e.g. "frontend engineer", "sales rep") ``` Roles and teams are **independent siblings** under an org — a role does not belong to a team in the data model. An actor can be tagged with both a `role_id` and a `team_id` at the same time. What connects them is the **memory promotion chain**: when memories are promoted up the hierarchy, the path is actor → role → team → org. Each level has its own `promotion_prompt` that biases extraction for what matters at that scope. Events tagged with `role_id` or `team_id` in `EventPayload` are extracted with the customised prompt for that role/team. ## Accessing the sub-resources --- ## Orgs --- ## Roles Roles represent distinct personas or job functions within your org. --- ## Teams Teams represent groups of actors that share a context. --- ## Tagging events with role/team Once the hierarchy is set up, tag events at ingest time. **This part works from any SDK or raw HTTP:** The memory extractor uses the role's/team's `promotion_prompt` to bias extraction toward relevant facts for that context. --- ## Response model All three sub-resources return shaped objects with the same fields (Python is `snake_case`, Node is `camelCase`): | Python | Node | Description | |---|---|---| | `org_id` / `role_id` / `team_id` | `orgId` / `roleId` / `teamId` | Stable resource ID. | | `name` | `name` | Display name. | | `focus` | `focus` | Description used to generate the promotion prompt. | | `promotion_prompt` | `promotionPrompt` | Auto-generated extraction prompt for this scope. | | `created_at` / `updated_at` | `createdAt` / `updatedAt` | ISO-8601 timestamps. | | `prompt_meta` | `promptMeta` | Metadata about the prompt generation. | See [Python models reference](/docs/reference/python/models) or [Node models reference](/docs/reference/node/models). --- ## See also - **[Ingesting Events](/docs/ingesting-events)** — `role_id` / `team_id` on `EventPayload`. - **[Console Memories](/docs/console-memories)** — browse memories scoped to roles/teams. - Reference: [`MemsyClient` (Python)](/docs/reference/python/memsy-client) · [`MemsyClient` (Node)](/docs/reference/node/memsy-client). --- ## Quickstart **URL:** https://docs.memsy.io/docs/quickstart **Description:** Sign up, install Memsy, and run your first ingest + search in under five minutes. Five minutes to your first memory. This page walks you through **dashboard → install → ingest → search** end-to-end. Code blocks below are tabbed for **Python**, **Node**, and **cURL** — pick your stack. See [Installation](/docs/installation) for `uv`, `poetry`, `pnpm`, `yarn`, and dev extras. ## What just happened? In three calls you: 1. **Ingested** raw events over HTTPS with Bearer-token auth. 2. **Memsy queued them** and ran extraction → embeddings → indexing on its worker pool. 3. **Searched** a semantic index and got back ranked results. And as a bonus: - Every authenticated response has `usage` (`X-Usage-*`) and `rateLimit` / `rate_limit` (`X-RateLimit-Limit`/`-Remaining`/`-Reset`) populated — you already know how much of your plan you've used and how many calls remain. - Rate-limited calls (HTTP 429) are retried automatically with exponential backoff. No code to write. - HTTP errors come back as typed exceptions in both SDKs (`AuthenticationError`/`MemsyAuthError`, `UsageLimitExceeded`, `MemsyRateLimitError`, …). ## What's next --- ## Retries and Timeouts **URL:** https://docs.memsy.io/docs/retries **Description:** Auto-retry on 429, how to tune it, and when to back off yourself. The SDK retries rate-limited (HTTP 429) requests automatically. Everything else is up to you. ## What the SDKs retry - **429 responses**, up to the `max_retries` / `maxRetries` count with exponential backoff. - If the response includes a `Retry-After` header, the SDKs wait exactly that long. - Otherwise they wait `1s, 2s, 4s, …` (Node uses `2 ** attempt` seconds with the defaults; Python uses `retry_backoff * 2 ** attempt`). ## What they don't retry - 4xx responses other than 429. These raise/throw immediately. - 5xx responses. These surface as `MemsyAPIError`; retry them yourself if appropriate. - Timeouts and connection errors. These raise `MemsyConnectionError`. ## Tuning retry behaviour ### When to increase `max_retries` For background workers and batch jobs where latency doesn't matter but making forward progress does. ### When to decrease `max_retries` For user-facing request paths where you'd rather fail fast and show an error than hang the request while the SDK sleeps. ### `retry_backoff` Tune if you're sharing one API key across many workers and want to spread retries out. In most single-client setups the default is fine. ## What happens after retries are exhausted The SDK raises a rate-limit exception with the `Retry-After` value populated if the server provided one. ## Idempotency considerations - `search()` and `status()` are read-only and always safe to retry. - `ingest()` is **safe to retry within a rolling 60-second window**. The platform hashes `(actor_id, session_id, kind, content)` and collapses identical tuples to a single stored event, returning the **original** `event_id` for duplicates so subsequent `status()` calls still work. Dedup is best-effort — under platform degradation you may still see fresh `event_id`s for duplicate submissions, so for retries outside the window or cross-process exactly-once needs, keep a client-side log of submitted hashes. - `clear()` is currently a stub that returns **501 Not Implemented** — it does not delete anything. A real actor-scoped delete is being designed; until then, calling `clear()` will raise `MemsyAPIError` (Python) / `MemsyAPIError` (Node). Avoid calling it in production paths. ## Timeouts `timeout` is passed straight through to `httpx`. It covers the full request lifecycle — connect + read + write. If a single call can take longer, raise it. Timeouts raise `MemsyConnectionError`, not `RateLimitExceeded`. ## Next - Back to the [API Reference](/docs/reference/python/memsy-client). --- ## Searching Memory **URL:** https://docs.memsy.io/docs/searching-memory **Description:** Natural-language queries, thresholds, scoping, and score interpretation. `search()` takes a natural-language query and returns ranked memories. ## Basic usage ## Parameters ### `query` Plain English. You don't need to phrase it like a keyword search — "things the user mentioned about their dog" works. ### `actor_id` / `actorId` Pass it to scope the search to one user/agent. Omit it to search across every actor in the organization. In most user-facing flows you want to pass the current actor. ### `limit` How many results to return. Memsy may return fewer if not enough pass the threshold. Valid range: 1–100. ### `threshold` A floor on the similarity score — results scoring below it are dropped. **The SDK default is `0.0` (no filter)**, so out of the box you get the top `limit` results ranked by relevance. Raise the threshold to cut noise. The right value depends on tier: - **Free (no reranking)**: raw retrieval scores cluster in `0.0`–`0.1` even for strong matches. Stay near `0.0`–`0.05`. - **Pro and above (with Cohere reranking)**: scores are normalised to `0.0`–`1.0`. A threshold around `0.3`–`0.5` becomes a meaningful "good match" floor; `0.8`+ returns only near-exact matches. Scores are not comparable across queries — `0.6` for query A is not the same thing as `0.6` for query B. Treat `threshold` as a relative tuning knob, not an absolute quality bar. ### `include_source_events` / `includeSourceEvents` When `true`, each result additionally includes the source events the memory was extracted from. Useful for showing provenance in a UI. ## The result shape `strength` is a reinforcement signal bounded `0.0`–`5.0` by the platform's policy ceiling; it rises by a small amount each time a memory is returned by `search()` and decays slowly when unused. **Not** a probability — don't normalise it into the `[0, 1]` range. `source_metadata` carries the JSON metadata you passed at ingest, attached to the originating event(s) the memory was derived from. Up to 5 entries; each entry is `{event_id, metadata: {...}}` for valid JSON-object payloads or `{event_id, raw: "..."}` otherwise. Use it for tagging, filtering, or correlating back to your application's own records. ## Reading usage and rate-limit info Every response carries observability metadata: See **[Usage and Rate Limits](/docs/usage-and-rate-limits)** for every field. ## Common patterns ### Personalisation at prompt time ### Cross-actor search (admin tools) Leave `actor_id`/`actorId` unset and you'll search every actor in the org. Good for analytics dashboards and support tooling; almost never what you want in an end-user-facing agent loop. ### Empty results `search()` always returns a response object. If nothing passed the threshold, `results.results` is `[]`. Handle that case in your UI. ## Limitations Search only returns memories that have finished processing. Very recent events may still be in the queue — see **[Async Processing](/docs/async-processing)**. ## Next - Run the same thing from asyncio (Python) → **[Async client](/docs/async-client)**. --- ## Sign Up & Get an API Key **URL:** https://docs.memsy.io/docs/signup **Description:** Create an account, provision an organization, and generate your first API key. Before you can use the SDK you need a Memsy account, an organization, and an API key. Everything below happens in the dashboard at **[app.memsy.io](https://app.memsy.io)**. ## Rotate or revoke a key Back in **Settings → API Keys** you can rotate any key at any time. A revoked key stops working immediately; rotation issues a new key and leaves the old one working until you delete it. ## Next → **[Install the package](/docs/installation)** --- ## Usage and Rate Limits **URL:** https://docs.memsy.io/docs/usage-and-rate-limits **Description:** Track plan consumption from response headers automatically. Every successful response from the Memsy API carries HTTP headers describing your current usage and rate-limit posture. The SDKs parse them into two typed objects on every response: - `usage` — a `UsageInfo`, from `X-Usage-*` and `X-Plan` headers. - `rate_limit` (Python) / `rateLimit` (Node) — a `RateLimitInfo`, from `X-RateLimit-*` headers. The rate-limit triple is **populated on every authenticated response** (its dimension is `api_calls`); `usage` may still be `None` / `null` on routes that don't carry usage headers (e.g. `/health`). ## Accessing usage Fields on `UsageInfo` (Python `snake_case` / Node `camelCase`): | Python | Node | Description | |---|---|---| | `api_calls` | `apiCalls` | Total API calls this billing period. | | `api_calls_limit` | `apiCallsLimit` | API call limit for your plan. | | `events_ingested` | `eventsIngested` | Events ingested this period. | | `events_ingested_limit` | `eventsIngestedLimit` | Ingestion limit. | | `memory_stored` | `memoryStored` | Memories currently stored. | | `memory_stored_limit` | `memoryStoredLimit` | Storage limit. | | `llm_tokens` | `llmTokens` | LLM tokens consumed during extraction. | | `llm_tokens_limit` | `llmTokensLimit` | Token limit. | | `search_queries` | `searchQueries` | Search queries issued this period. | | `search_queries_limit` | `searchQueriesLimit` | Query limit. | | `plan` | `plan` | Your plan name (e.g. `"free"`, `"pro"`). | ## Accessing rate limits Fields on `RateLimitInfo`: | Field | Description | |---|---| | `limit` | Plan limit for the `api_calls` dimension this billing period. | | `remaining` | Calls remaining in the current period (clamped at `0`). | | `reset` | Unix timestamp (UTC) of the next billing-period boundary — the start of the next month. | ## Underlying headers | Header | Field | |---|---| | `X-Usage-ApiCalls` | `usage.api_calls` | | `X-Usage-ApiCalls-Limit` | `usage.api_calls_limit` | | `X-Usage-EventsIngested` | `usage.events_ingested` | | `X-Usage-EventsIngested-Limit` | `usage.events_ingested_limit` | | `X-Usage-MemoriesStored` | `usage.memory_stored` | | `X-Usage-MemoriesStored-Limit` | `usage.memory_stored_limit` | | `X-Usage-LlmTokens` | `usage.llm_tokens` | | `X-Usage-LlmTokens-Limit` | `usage.llm_tokens_limit` | | `X-Usage-SearchQueries` | `usage.search_queries` | | `X-Usage-SearchQueries-Limit` | `usage.search_queries_limit` | | `X-Plan` | `usage.plan` | | `X-RateLimit-Limit` | `rate_limit.limit` | | `X-RateLimit-Remaining` | `rate_limit.remaining` | | `X-RateLimit-Reset` | `rate_limit.reset` | ## Patterns ### Log usage to your observability stack ### Alert before you hit a cap ## When usage is missing ## Next - What happens when you blow a cap → **[Error handling](/docs/error-handling)**. - How the SDK auto-handles 429s → **[Retries](/docs/retries)**. --- ## MemsyControlClient **URL:** https://docs.memsy.io/docs/reference/node/control-client **Description:** The Node.js MemsyControlClient — control-plane endpoints (api/) for identity, billing, keys, usage, events, and Pro interest. `MemsyControlClient` is a separate client for the Memsy **control plane** (`api/`), distinct from the hot-path `MemsyClient` (`v1/`). Use it for account management, API key lifecycle, billing, usage reporting, and event browsing. ```ts const control = new MemsyControlClient({ baseUrl: process.env.MEMSY_CONTROL_URL!, apiKey: process.env.MEMSY_API_KEY!, }); ``` ## Constructor ```ts new MemsyControlClient(options: MemsyControlClientOptions) ``` Same shape as [`MemsyClientOptions`](/docs/reference/node/memsy-client#memsyclientoptions): `baseUrl` (required), `apiKey` (required), `timeoutMs` (default `30_000`), `maxRetries` (default `3`). ## Top-level methods ### `me()` Identity information for the authenticated caller. ```ts const me = await control.me(); console.log(me.orgId, me.tier, me.email); ``` Returns a [`MeResponse`](/docs/reference/node/models#meresponse). ### `health()` Control-plane liveness check. ```ts const h = await control.health(); console.log(h.status, h.version); ``` Returns a [`HealthResponse`](/docs/reference/node/models#healthresponse). ## Sub-resources ### `control.keys` API key CRUD. **Admin-gated** — non-admin keys raise `MemsyAuthorizationError`. ```ts const keys = await control.keys.list(); // ApiKeyListResponse const created = await control.keys.create('ci-key', { scopes: ['read'] }); console.log(created.rawKey); // shown ONCE — store securely await control.keys.delete(created.keyId); const usage = await control.keys.usage('key_id'); ``` | Method | Signature | |---|---| | `list()` | `Promise` | | `create(name, options?)` | `Promise` — `options.scopes`, `options.expiresAt` | | `delete(keyId)` | `Promise` | | `usage(keyId)` | `Promise[]>` | ### `control.usage` Plan usage summary and timeseries. **Admin-gated.** ```ts const summary = await control.usage.summary(); const series = await control.usage.timeseries({ granularity: 'daily', dimension: 'api_calls', periodStart: '2026-01-01', periodEnd: '2026-01-31', }); ``` | Method | Signature | |---|---| | `summary()` | `Promise` | | `timeseries(options?)` | `Promise` | ### `control.billing` Stripe-backed billing summary and invoices. **Admin-gated.** May raise `MemsyBillingNotEnabledError` on free-tier orgs. ```ts const summary = await control.billing.summary(); // BillingSummary const invoices = await control.billing.invoices(); // Invoice[] ``` | Method | Signature | |---|---| | `summary()` | `Promise` | | `invoices()` | `Promise` | ### `control.events` Browse raw ingested events from the control-plane console. Requires an assigned seat — non-seated keys raise `MemsySeatRequiredError`. ```ts const r = await control.events.list({ actorId: 'user_42', sessionId: 'sess_abc', kind: 'user_message', sort: 'ts_desc', limit: 50, offset: 0, }); ``` | Method | Signature | |---|---| | `list(options?)` | `Promise` | ### `control.interest` Express interest in the Pro plan, or check whether your org has already done so. ```ts const ok = await control.interest.express('me@example.com', 'Acme', { company: 'Acme Inc', useCase: 'agent memory for support bots', }); const expressed = await control.interest.status(); // boolean ``` | Method | Signature | |---|---| | `express(email, name, options?)` | `Promise` | | `status()` | `Promise` | ## Error classes you may see `MemsyAuthError` (401), `MemsyAuthorizationError` (admin/seat scope mismatch on 403), `MemsyBillingNotEnabledError` (free-tier hitting billing routes), `MemsyKeyLimitReachedError`, `MemsySeatLimitReachedError`. See [Errors](/docs/reference/node/errors) for the full list and the per-class fields. ## See also - [Models reference](/docs/reference/node/models) — `MeResponse`, `BillingSummary`, `ApiKeyInfo`, `EventItem`, … - [`MemsyClient`](/docs/reference/node/memsy-client) — the hot-path client - Python equivalents: [`MemsyControlClient` (Python)](/docs/reference/python/control-client) --- ## Errors **URL:** https://docs.memsy.io/docs/reference/node/errors **Description:** The Node SDK exception hierarchy — what gets thrown and how to catch it. All errors thrown by the Node SDK extend `MemsyError`. Both `MemsyClient` and `MemsyControlClient` raise the same hierarchy. Catching the right subclass lets you distinguish a transient connection issue from auth failure from a hit rate limit from a feature-gated endpoint. ```ts MemsyError, MemsyAPIError, MemsyConnectionError, MemsyAuthError, MemsyAuthorizationError, MemsyFeatureNotAvailableError, MemsyOrgIdNotAllowedError, MemsySeatRequiredError, MemsyOrgLimitReachedError, MemsyKeyLimitReachedError, MemsyBillingNotEnabledError, MemsySeatLimitReachedError, MemsyRateLimitError, MemsyUsageLimitExceededError, } from '@memsy-io/memsy'; ``` ## The hierarchy ``` MemsyError ├── MemsyConnectionError // network error or timeout └── MemsyAPIError // non-2xx HTTP response ├── MemsyAuthError // 401 — invalid or missing API key ├── MemsyAuthorizationError // 403 — key lacks the required scope ├── MemsyFeatureNotAvailableError // 403 — feature not on your tier ├── MemsyOrgIdNotAllowedError // 400 — free-tier: org_id not allowed in request ├── MemsySeatRequiredError // 403 — endpoint requires an assigned seat ├── MemsyOrgLimitReachedError // 403 — org tier limit reached ├── MemsyKeyLimitReachedError // 403 — API key tier limit reached ├── MemsyBillingNotEnabledError // 403 — billing not enabled for this org ├── MemsySeatLimitReachedError // 409 — purchased seat limit reached ├── MemsyRateLimitError // 429 — rate limit hit (after retries) └── MemsyUsageLimitExceededError // 429 — plan quota exceeded ``` ## Common base fields Every `MemsyAPIError` (and its subclasses) carries: | Field | Type | Description | |--------------|------------------|----------------------------------------------------------| | `statusCode` | `number` | HTTP status code. | | `detail` | `string` | Human-readable detail from the response body. | | `errorCode` | `string \| null` | Stable machine-readable code (e.g. `"feature_not_available"`). | | `response` | `Response \| null` | The raw `fetch` Response for further inspection. | ## Error-specific fields ### `MemsyAuthorizationError` | Field | Type | Description | |-----------------|------------------|---------------------------------------------------| | `requiredScope` | `string \| null` | Scope your API key is missing, if known. | ### `MemsyFeatureNotAvailableError` | Field | Type | Description | |---------------|------------------|-------------------------------------------------| | `feature` | `string \| null` | The gated feature's identifier. | | `currentTier` | `string \| null` | The tier of the API key that tried to use it. | | `upgradeUrl` | `string \| null` | Deep-link to the dashboard billing page. | ### `MemsyOrgLimitReachedError` / `MemsyKeyLimitReachedError` | Field | Type | Description | |------------|------------------|-------------------| | `limit` | `number \| null` | Tier limit. | | `current` | `number \| null` | Current count. | ### `MemsyBillingNotEnabledError` | Field | Type | Description | |----------------|------------------|------------------------------------------------| | `interestPath` | `string \| null` | API path to express Pro plan interest. | ### `MemsySeatLimitReachedError` | Field | Type | Description | |-------------------|------------------|----------------------------| | `purchasedSeats` | `number \| null` | Total purchased seats. | | `assignedSeats` | `number \| null` | Currently assigned seats. | | `pendingInvites` | `number \| null` | Pending invite count. | ### `MemsyRateLimitError` | Field | Type | Description | |--------------|------------------|---------------------------------------------------------------| | `retryAfter` | `number \| null` | Seconds the server suggested waiting (parsed `Retry-After`). | ### `MemsyUsageLimitExceededError` | Field | Type | Description | |--------------|------------------|----------------------------------------------------------| | `dimension` | `string \| null` | What quota was exhausted (e.g. `"events_ingested"`). | | `current` | `number \| null` | Current usage count. | | `limit` | `number \| null` | The limit that was exceeded. | | `upgradeUrl` | `string \| null` | Deep-link to upgrade. | ## A full example ```ts MemsyClient, MemsyAuthError, MemsyAuthorizationError, MemsyFeatureNotAvailableError, MemsyRateLimitError, MemsyUsageLimitExceededError, MemsyConnectionError, MemsyAPIError, } from '@memsy-io/memsy'; const client = new MemsyClient({ baseUrl: '...', apiKey: '***' }); try { const results = await client.search('preferences'); } catch (err) { if (err instanceof MemsyAuthError) { await refreshApiKey(); } else if (err instanceof MemsyAuthorizationError) { console.log(`Missing required scope: ${err.requiredScope}`); } else if (err instanceof MemsyFeatureNotAvailableError) { console.log(`Feature '${err.feature}' requires upgrade from ${err.currentTier}`); console.log(`Upgrade at: ${err.upgradeUrl}`); } else if (err instanceof MemsyRateLimitError) { console.log(`Rate limited; retry after ${err.retryAfter ?? 'unknown'}s`); } else if (err instanceof MemsyUsageLimitExceededError) { console.log(`Hit ${err.dimension} limit (${err.current}/${err.limit})`); console.log(`Upgrade at: ${err.upgradeUrl}`); } else if (err instanceof MemsyConnectionError) { /* retry with backoff */ } else if (err instanceof MemsyAPIError) { console.log(`API error ${err.statusCode}: ${err.detail}`); } else { throw err; } } ``` ## Catch broadly or narrowly? - **Request handlers / agent loops** — catch `MemsyError` and surface a friendly error. Log the specific subclass for debugging. - **Pipelines with budgets** — handle `MemsyUsageLimitExceededError` and `MemsyFeatureNotAvailableError` distinctly so you can route to an alert channel and link the upgrade page. - **Retryable background jobs** — catch `MemsyConnectionError` and `MemsyRateLimitError` separately from API-level errors; they're the only ones that "just work" on a later retry. - **Control-plane admin code** — handle `MemsyBillingNotEnabledError` and `MemsySeatLimitReachedError` and link users to the appropriate dashboard flow. ## See also - [`MemsyClient`](/docs/reference/node/memsy-client) — methods that throw these errors - [Error Handling guide](/docs/error-handling) — practical patterns for production apps --- ## MemsyClient **URL:** https://docs.memsy.io/docs/reference/node/memsy-client **Description:** The Node.js MemsyClient — constructor, methods, and return shapes. Async-only, Promise-based, ESM + CJS. `MemsyClient` is the single entry point for the Node SDK. Every method returns a `Promise ## Common patterns ### Read usage and rate-limit info Every successful call returns the parsed response headers as `usage` and `rateLimit`: ```ts const { usage, rateLimit } = await client.search('preferences'); if (rateLimit.remaining !== null && rateLimit.remaining < 5) { console.warn(`Only ${rateLimit.remaining} requests left this window.`); } ``` ### Reuse a single client The client is cheap to construct but reuse is preferred — it lets the runtime pool HTTP keep-alives. Construct once at module load, share across handlers. ```ts // lib/memsy.ts export const memsy = new MemsyClient({ baseUrl: process.env.MEMSY_BASE_URL!, apiKey: process.env.MEMSY_API_KEY!, }); ``` ### Handle errors See the [error reference](/docs/reference/node/errors) for the full hierarchy. ```ts MemsyAuthError, MemsyRateLimitError, MemsyAPIError, } from '@memsy-io/memsy'; try { await client.search('preferences'); } catch (err) { if (err instanceof MemsyAuthError) { /* refresh credentials */ } else if (err instanceof MemsyRateLimitError) { /* exponential backoff already done; queue for later */ } else if (err instanceof MemsyAPIError) { console.error(err.statusCode, err.detail); } else { throw err; } } ``` ## See also - [Models](/docs/reference/node/models) — request/response type definitions - [Errors](/docs/reference/node/errors) — exception hierarchy - [Error Handling guide](/docs/error-handling) — patterns for catching the right error --- ## Models & Types **URL:** https://docs.memsy.io/docs/reference/node/models **Description:** Request and response type definitions for the Node SDK. All public types are exported from the package root: ```ts EventPayload, EventKind, IngestResponse, SearchResponse, SearchResult, SourceEvent, StatusResponse, HealthResponse, ClearResponse, UsageInfo, RateLimitInfo, } from '@memsy-io/memsy'; ``` The SDK is camelCase end-to-end. The HTTP wire format is snake_case (`actor_id`, `session_id`, `event_ids`); serialization happens automatically inside the client. ## Request types ### `EventPayload` The shape you pass to [`client.ingest()`](/docs/reference/node/memsy-client#ingestevents). | Field | Type | Required | Description | |--------------|--------------|----------|-------------------------------------------------------------------| | `actorId` | `string` | yes | Stable identifier for the entity producing the event. | | `sessionId` | `string` | yes | Session/conversation grouping ID. | | `kind` | `EventKind` | yes | One of the literal values below. | | `content` | `string` | yes | The event body — message text, tool output, etc. | | `roleId` | `string` | no | Optional role attribution (e.g. `'admin'`). | | `teamId` | `string` | no | Optional team scoping. | | `ts` | `string` | no | ISO-8601 timestamp. Defaults to server-side now. | | `metadata` | `string` | no | Free-form JSON-encoded string for app-specific metadata. | ### `EventKind` ```ts type EventKind = | 'user_message' | 'assistant_message' | 'tool_result' | 'app_event'; ``` See [Events & Memories](/docs/events-and-memories) for guidance on which to use. ## Response types Every successful response carries `usage` and `rateLimit` parsed from response headers — the call returned, but you also know how much of your plan you've burned. ### `IngestResponse` | Field | Type | Description | |-------------|-----------------------------|------------------------------------------------------| | `eventIds` | `string[]` | One ID per event, in the order you submitted them. | | `usage` | `UsageInfo \| null` | Plan usage snapshot from response headers. | | `rateLimit` | `RateLimitInfo \| null` | Rate-limit window state. | ### `SearchResponse` | Field | Type | Description | |-------------|---------------------------|--------------------------------------| | `results` | `SearchResult[]` | Ranked memory matches. | | `usage` | `UsageInfo \| null` | Plan usage snapshot. | | `rateLimit` | `RateLimitInfo \| null` | Rate-limit window state. | ### `SearchResult` | Field | Type | Description | |-----------------|---------------------------------------|-----------------------------------------------------------------------| | `id` | `string` | Memory ID. | | `content` | `string` | Extracted memory text. | | `score` | `number` | Similarity score `0.0` (none) – `1.0` (exact). | | `metadata` | `Record \| null` | Server-attached fields: `type`, `kind`, `strength`, `confidence`, `entities`, `source_metadata`, … | | `sourceEvents` | `SourceEvent[] \| null` | Originating events — populated when `includeSourceEvents: true`. | | `sourceMetadata`| `SourceMetadata[] \| null` | User-supplied metadata from source event(s); capped at 5 entries. | ### `SourceMetadata` User-supplied event metadata propagated to memories derived from those events. Surfaced on `SearchResult.sourceMetadata`. | Field | Type | Description | |------------|-----------------------------------|-----------------------------------------------------------------------------| | `eventId` | `string` | The originating event's ID. | | `metadata` | `Record` (opt.) | Parsed object when the event's `metadata` string was valid JSON object. | | `raw` | `string` (opt.) | Original string when the event's `metadata` was not a JSON object. | ### `SourceEvent` A typed view of the events a memory was extracted from. Surfaced on `SearchResult.sourceEvents` whenever `includeSourceEvents: true` is passed to `client.search()`. The wire format embeds these as snake-cased dicts inside `metadata.source_events`; the SDK parses them and converts to camelCase for you. | Field | Type | Description | |------------|------------------|--------------------------------------------| | `eventId` | `string` | The ID of the originating event. | | `kind` | `string` | `EventKind` value of the originating event.| | `content` | `string` | Original event body. | | `ts` | `string \| null` | ISO-8601 timestamp. | ```ts const results = await client.search('preferences', { actorId: 'user_42', includeSourceEvents: true, }); for (const r of results.results) { console.log(r.content); for (const src of r.sourceEvents ?? []) { console.log(` ↳ from ${src.kind} at ${src.ts}: ${src.content}`); } } ``` ### `StatusResponse` | Field | Type | Description | |----------------|--------------------------------------------|-------------------------------------------------------------| | `completedIds` | `string[]` | Events whose memories are indexed and searchable. | | `failedIds` | `string[]` | Events whose extraction failed. | | `pendingIds` | `string[]` | Events still being processed. | | `total` | `number` | Sum of the three lists (matches your input length). | | `statuses` | `Record \| null` | Per-event status string when available. | | `usage` | `UsageInfo \| null` | | | `rateLimit` | `RateLimitInfo \| null` | | ### `HealthResponse` | Field | Type | Description | |------------------|-----------------------------------------|---------------------------------------------------| | `status` | `string` | `'ok'` when healthy. | | `version` | `string` | Server version string. | | `billingEnabled` | `boolean \| null` | Whether billing is configured for this org. | | `components` | `Record \| null` | Per-component health (db, redis, etc.). | | `usage` | `UsageInfo \| null` | | | `rateLimit` | `RateLimitInfo \| null` | | ### `ClearResponse` | Field | Type | Description | |-------------|----------------------------|---------------------------------------------| | `deleted` | `number` | Count of records removed. | | `usage` | `UsageInfo \| null` | | | `rateLimit` | `RateLimitInfo \| null` | | ## Metadata types ### `UsageInfo` Snapshot of your plan consumption parsed from `X-Usage-*` and `X-Plan` headers. | Field | Type | Description | |------------------------|------------------|---------------------------------------------| | `apiCalls` | `number \| null` | API calls used this period. | | `apiCallsLimit` | `number \| null` | API calls allowed this period. | | `eventsIngested` | `number \| null` | Events ingested this period. | | `eventsIngestedLimit` | `number \| null` | | | `memoryStored` | `number \| null` | Memories currently stored. | | `memoryStoredLimit` | `number \| null` | | | `llmTokens` | `number \| null` | LLM tokens consumed for extraction. | | `llmTokensLimit` | `number \| null` | | | `searchQueries` | `number \| null` | Search queries issued this period. | | `searchQueriesLimit` | `number \| null` | | | `plan` | `string \| null` | Plan name (e.g. `'free'`, `'pro'`). | Any field is `null` when the platform hasn't reported it on this response. ### `RateLimitInfo` Parsed from `X-RateLimit-*` headers. | Field | Type | Description | |--------------|------------------|-----------------------------------------------------------------| | `limit` | `number \| null` | Max requests allowed in the current window. | | `remaining` | `number \| null` | Calls remaining in the current period (clamped at 0). | | `reset` | `number \| null` | Unix timestamp (seconds) of the next billing-period boundary. | Populated on every authenticated response — the rate-limit dimension is `api_calls`. ```ts const { rateLimit } = await client.search('preferences'); if (rateLimit.remaining !== null && rateLimit.remaining < 5) { console.warn(`Only ${rateLimit.remaining} requests left.`); } ``` ## Onboarding types ### `Org`, `Role`, `Team` All three share a common shape and differ only in the ID fields. | Field | Type | Notes | |-------------------|-----------------------------------|----------------------------------------------------| | `orgId` | `string` | All three. | | `roleId` | `string` | `Role` only. | | `teamId` | `string` | `Team` only. | | `name` | `string` | Display name. | | `focus` | `string` | Description used to generate the promotion prompt. | | `promotionPrompt` | `string` | Auto-generated extraction prompt. | | `createdAt` | `string` | ISO-8601 timestamp. | | `updatedAt` | `string` | ISO-8601 timestamp. | | `promptMeta` | `Record \| null` | Metadata about prompt generation. | ## Console memory types ### `MemoryItem` | Field | Type | Description | |----------------------|-----------------------------------|----------------------------------------------| | `memoryId` | `string` | Unique memory ID. | | `orgId` | `string` | Owning org. | | `scope` | `MemoryScope` | Scope info (level + actor/team/role IDs). | | `type` | `string` | E.g. `"preference"`, `"fact"`. | | `kind` | `string` | E.g. `"semantic"`, `"episodic"`. | | `memoryKind` | `string` | Low-level kind classification. | | `status` | `string` | `"active"` / `"archived"` / `"decayed"`. | | `text` | `string` | The extracted memory text. | | `confidence` | `number` | Extraction confidence (0–1). | | `strength` | `number` | Reinforcement strength (0–5.0, default 1.0). Not a probability. | | `recallCount` | `number` | Times recalled in search. | | `decayHalfLifeDays` | `number` | Days until strength halves. | | `pinned` | `boolean` | Pinned memories never decay. | | `tags` | `string[]` | Tags from extraction. | | `entityRefs` | `Record[]` | Named entity references. | | `sourceEventIds` | `string[]` | Contributing event IDs. | | `sourceUrls` | `string[]` | Source URLs if applicable. | | `summary` | `string \| null` | Auto-generated summary. | | `payload` | `Record \| null` | Raw extraction payload. | | `lastRecalledAt` | `string \| null` | ISO-8601 timestamp. | | `effectiveFrom` | `string \| null` | ISO-8601 timestamp. | | `effectiveTo` | `string \| null` | ISO-8601 timestamp. | | `observedAt` | `string \| null` | ISO-8601 timestamp. | | `createdAt` | `string \| null` | ISO-8601 timestamp. | | `updatedAt` | `string \| null` | ISO-8601 timestamp. | ### `MemoryScope` | Field | Type | Description | |------------|------------------|------------------------------------------------------| | `level` | `string` | `"org"` / `"role"` / `"team"` / `"actor"`. | | `actorId` | `string \| null` | | | `teamId` | `string \| null` | | | `roleId` | `string \| null` | | ### `MemoryListResponse` / `MemoryStatsResponse` `MemoryListResponse` is `{ items: MemoryItem[]; total: number; limit: number; offset: number }`. `MemoryStatsResponse` carries aggregate counts: `total`, `totalMemories`, `activeMemories`, `byType`, `byKind`, `byStatus`, `avgConfidence`, `avgStrength`, `topEntities`, `confidenceBuckets`, `dateRange`. ## Control-plane types ### `MeResponse` | Field | Type | Description | |------------------|------------------|------------------------------------------| | `customerId` | `string` | | | `email` | `string` | | | `tier` | `string` | E.g. `"free"`, `"pro"`. | | `isSuperadmin` | `boolean` | | | `orgId` | `string` | | | `isBillingAdmin` | `boolean` | | | `userId` | `string \| null` | | | `orgRole` | `string \| null` | | ### `BillingSummary` `{ tier, purchasedSeats, assignedSeats, availableSeats, stripeCustomerId, paymentMethod, upcomingInvoice, subscriptionStatus, billingContact, stripeSubscriptionId }`. `PaymentMethod` is `{ brand, last4, expMonth, expYear }`. `UpcomingInvoice` is `{ amountDue, currency, periodEnd }`. ### `Invoice` `{ id, amountDue, amountPaid, currency, status, created, hostedInvoiceUrl }`. ### `ApiKeyInfo` / `ApiKeyListResponse` / `CreateKeyResponse` ```ts ApiKeyInfo: { keyId, prefix, name, scopes, isActive, createdAt, lastUsedAt, expiresAt } ApiKeyListResponse: { keys: ApiKeyInfo[], maxKeys, activeCount } CreateKeyResponse: { keyId, rawKey, prefix, name, scopes } ``` `rawKey` is returned **only once** on create — store it securely. ### `UsageSummaryResponse` / `UsageTimeseriesResponse` ```ts DimensionUsage: { dimension, used, limit, overageRate } UsageSummaryResponse: { orgId, tier, periodStart, periodEnd, dimensions: DimensionUsage[] } TimeseriesPoint: { date, dimension, quantity } UsageTimeseriesResponse: { orgId, granularity, data: TimeseriesPoint[] } ``` ### `EventItem` / `EventListResponse` ```ts EventItem: { eventId, orgId, actorId, kind, content, ts, sessionId, metadata, ingestedAt } EventListResponse: { items: EventItem[], total, limit, offset } ``` ### `ProInterestResponse` `{ message: string }`. ## See also - [`MemsyClient`](/docs/reference/node/memsy-client) — hot-path client - [`MemsyControlClient`](/docs/reference/node/control-client) — control-plane client - [Errors](/docs/reference/node/errors) — exception classes --- ## Sub-resources **URL:** https://docs.memsy.io/docs/reference/node/resources **Description:** orgs, roles, teams, and memories — sub-resources on the Node MemsyClient. `MemsyClient` exposes four sub-resources for the onboarding hierarchy and console memory browsing. They behave identically to the Python equivalents — same endpoints, same parameter shapes, with camelCased field names and `Promise` returns. ```ts const client = new MemsyClient({ baseUrl: process.env.MEMSY_BASE_URL!, apiKey: process.env.MEMSY_API_KEY!, }); client.orgs; // OrgsResource client.roles; // RolesResource client.teams; // TeamsResource client.memories; // MemoriesResource ``` Most onboarding mutation endpoints (create/update/regenerate/delete) are **admin-gated**. Non-admin keys raise [`MemsyAuthorizationError`](/docs/reference/node/errors#memsyauthorizationerror). ## `client.orgs` ```ts await client.orgs.list(); // Org[] await client.orgs.create(orgId, name, focus); // Org await client.orgs.get(orgId); // Org await client.orgs.update(orgId, { name, focus, promotionPrompt }); // Org await client.orgs.regeneratePrompt(orgId); // Org await client.orgs.delete(orgId); // void ``` | Method | Signature | |---|---| | `list()` | `Promise` | | `create(orgId, name, focus)` | `Promise` | | `get(orgId)` | `Promise` | | `update(orgId, update)` | `Promise` — `OrgUpdate` | | `regeneratePrompt(orgId)` | `Promise` | | `delete(orgId)` | `Promise` | `OrgUpdate` is `{ name?, focus?, promotionPrompt? }`. ## `client.roles` Roles always require an `orgId`. ```ts await client.roles.list(orgId, { limit, offset }); // Role[] await client.roles.create(orgId, name, focus); // Role await client.roles.get(roleId, orgId); // Role await client.roles.update(roleId, orgId, { name, focus, promotionPrompt }); // Role await client.roles.regeneratePrompt(roleId, orgId); // Role await client.roles.delete(roleId, orgId); // void ``` ## `client.teams` Same shape as `roles` — every method takes an `orgId`. ```ts await client.teams.list(orgId, { limit, offset }); // Team[] await client.teams.create(orgId, name, focus); // Team await client.teams.get(teamId, orgId); // Team await client.teams.update(teamId, orgId, { name, focus, promotionPrompt }); // Team await client.teams.regeneratePrompt(teamId, orgId); // Team await client.teams.delete(teamId, orgId); // void ``` ## `client.memories` Read-only console memory browsing. ```ts await client.memories.list({ // MemoryListResponse actorId: 'user_42', // filter to a single actor kind: 'semantic', type: 'preference', status: 'active', sort: 'observed_at_desc', search: 'dark mode', limit: 50, offset: 0, }); await client.memories.stats(); // MemoryStatsResponse await client.memories.get(memoryId); // MemoryItem ``` Pagination is stable across pages: the server always sorts with `memory_id` as a secondary tiebreaker, so walking `offset` won't return duplicates or skip records. | Method | Signature | |---|---| | `list(options?)` | `Promise` — `MemoryListOptions` | | `stats()` | `Promise` | | `get(memoryId)` | `Promise` | `MemoryListOptions` is `{ kind?, type?, status?, sort?, search?, limit?, offset? }`. ## See also - [Models reference](/docs/reference/node/models) — full shape of `Org`, `Role`, `Team`, `MemoryItem`, etc. - [Onboarding guide](/docs/onboarding) — when and why to set up the hierarchy - [Console Memories guide](/docs/console-memories) — patterns for browsing/auditing memories --- ## AsyncMemsyClient **URL:** https://docs.memsy.io/docs/reference/python/async-memsy-client **Description:** Async HTTP client — mirrors MemsyClient method-for-method. Asynchronous HTTP client for the Memsy API. Mirrors **[`MemsyClient`](/docs/reference/python/memsy-client)** method-for-method. ```python from memsy import AsyncMemsyClient ``` ## Constructor ```python AsyncMemsyClient( base_url: str, api_key: str, timeout: float = 30.0, max_retries: int = 3, retry_backoff: float = 1.0, ) ``` Parameters are identical to `MemsyClient`. See [that page](/docs/reference/python/memsy-client) for the full table. ## Async context manager ```python async with AsyncMemsyClient(base_url="...", api_key="***") as client: await client.health() ``` If you can't use `async with`, call `await client.close()` explicitly. ## Methods Each method has the same signature as the sync client but returns a coroutine. | Method | Returns | |---|---| | `await ingest(events)` | `IngestResponse` | | `await search(query, *, actor_id, limit, threshold, include_source_events)` | `SearchResponse` | | `await status(event_ids)` | `StatusResponse` | | `await health()` | `HealthResponse` | | `await clear(container_tag)` | `ClearResponse` (currently a `501 Not Implemented` stub — see [retries notes](/docs/retries#idempotency-considerations)) | | `await close()` | `None` | All raise the same exceptions as the sync client. ## Sub-resource accessors `AsyncMemsyClient` exposes async versions of all sub-resources. | Accessor | Methods | |---|---| | `client.orgs` | `await list()`, `await create(...)`, `await get(...)`, `await update(...)`, `await regenerate_prompt(...)`, `await delete(...)` | | `client.roles` | `await list(org_id)`, `await create(...)`, `await get(...)`, `await update(...)`, `await regenerate_prompt(...)`, `await delete(...)` | | `client.teams` | `await list(org_id)`, `await create(...)`, `await get(...)`, `await update(...)`, `await regenerate_prompt(...)`, `await delete(...)` | | `client.memories` | `await list(...)`, `await get(memory_id)`, `await stats()` | See **[`MemsyClient`](/docs/reference/python/memsy-client)** for the full method signatures — async versions are identical except for `await`. ## Parallel calls ```python async def two_in_parallel(client: AsyncMemsyClient, user_id: str): prefs, recent = await asyncio.gather( client.search("preferences", actor_id=user_id), client.search("recent topics", actor_id=user_id), ) return prefs.results, recent.results ``` ## See also - **[Async client guide](/docs/async-client)** — FastAPI/Starlette integration patterns. - **[`MemsyClient`](/docs/reference/python/memsy-client)** — the synchronous twin. - **[`AsyncMemsyControlClient`](/docs/reference/python/control-client)** — async control-plane client. --- ## MemsyControlClient **URL:** https://docs.memsy.io/docs/reference/python/control-client **Description:** Control-plane client for billing, API keys, usage, and account management. `MemsyControlClient` and `AsyncMemsyControlClient` wrap the Memsy control-plane API (`api/`). This is a separate service from the hot-path memory engine — it handles account management, billing, API key lifecycle, and usage reporting. ```python from memsy import MemsyControlClient, AsyncMemsyControlClient ``` ## When to use this client Use `MemsyControlClient` when you need to: - Look up account or org identity (`me()`) - Read usage metrics and billing summaries - Programmatically create or rotate API keys (admin-only) - Browse ingested events in the console - Express interest in the Pro plan For memory operations (ingest, search, clear), use **[`MemsyClient`](/docs/reference/python/memsy-client)** instead. ## Constructor ```python MemsyControlClient( base_url: str, api_key: str, timeout: float = 30.0, max_retries: int = 3, retry_backoff: float = 1.0, ) ``` | Parameter | Type | Default | Description | |---|---|---|---| | `base_url` | `str` | — | Control-plane base URL, e.g. `"https://api.memsy.io/api"`. | | `api_key` | `str` | — | API key (`msy_...`). Sent as `Authorization: Bearer `. | | `timeout` | `float` | `30.0` | Request timeout in seconds. | | `max_retries` | `int` | `3` | Max automatic retries on HTTP 429. | | `retry_backoff` | `float` | `1.0` | Base exponential backoff in seconds. | ## Context manager ```python with MemsyControlClient( base_url=os.environ["MEMSY_CONTROL_URL"], api_key=os.environ["MEMSY_API_KEY"], ) as control: me = control.me() ``` ## Top-level methods ### `me()` Return identity information for the authenticated API key. ```python me() -> MeResponse ``` ```python me = control.me() print(me.email, me.tier, me.org_role) ``` ### `health()` Check if the control-plane is healthy. ```python health() -> HealthResponse ``` --- ## Sub-resource accessors ### `control.usage` Usage metrics for the current org. ```python # Summary for the current billing period summary: UsageSummaryResponse = control.usage.summary() # Timeseries (daily by default) ts: UsageTimeseriesResponse = control.usage.timeseries( dimension="events_ingested", # optional filter granularity="daily", # "daily" | "hourly" period_start="2026-04-01", # optional ISO-8601 date period_end="2026-04-30", ) ``` ### `control.billing` Billing summary and invoice history. ```python summary: BillingSummary = control.billing.summary() invoices: list[Invoice] = control.billing.invoices() ``` ### `control.keys` API key management. **Requires `org_role == "org:admin"`** — plain API keys will receive `AuthorizationError`. ```python # List all keys (includes quota info) key_list: ApiKeyListResponse = control.keys.list() # Create a new key new_key: CreateKeyResponse = control.keys.create( name="ci-deploy", scopes=["read", "write"], expires_at="2027-01-01T00:00:00Z", # optional ) print(new_key.raw_key) # shown once — store immediately # Per-key usage stats (returns list[dict] — raw usage records) usage: list[dict] = control.keys.usage(key_id) # Revoke a key control.keys.delete(key_id) ``` ### `control.events` Browse raw ingested events for your org (console view). ```python events: EventListResponse = control.events.list( actor_id="user_42", # optional filter session_id="session_1", # optional filter kind="user_message", # optional filter sort="ts_desc", # "ts_desc" | "ts_asc" limit=50, offset=0, ) ``` ### `control.interest` Express interest in the Pro plan or check waitlist status. ```python # Express interest resp: ProInterestResponse = control.interest.express( email="you@example.com", name="Your Name", company="Acme Inc", # optional use_case="AI agent memory", # optional ) # Check whether your org has already expressed interest already_expressed: bool = control.interest.status() ``` --- ## Async variant `AsyncMemsyControlClient` is the async counterpart. Every method is identical but returns a coroutine. ```python async with AsyncMemsyControlClient( base_url=os.environ["MEMSY_CONTROL_URL"], api_key=os.environ["MEMSY_API_KEY"], ) as control: me = await control.me() summary = await control.usage.summary() events = await control.events.list(limit=20) ``` Sub-resources (`control.usage`, `control.billing`, `control.keys`, `control.events`, `control.interest`) are all available with `await` equivalents. --- ## See also - **[`MemsyClient`](/docs/reference/python/memsy-client)** — hot-path memory client. - **[Models](/docs/reference/python/models)** — all control-plane response dataclasses. - **[Exceptions](/docs/reference/python/exceptions)** — `BillingNotEnabledError`, `KeyLimitReachedError`, `SeatLimitReachedError`, and others raised by this client. --- ## Exceptions **URL:** https://docs.memsy.io/docs/reference/python/exceptions **Description:** Every error class the SDK can raise. All SDK errors are importable from `memsy.exceptions`. The SDK also re-exports them from the top-level `memsy` package. ```python from memsy.exceptions import ( MemsyError, MemsyConnectionError, MemsyAPIError, AuthenticationError, AuthorizationError, FeatureNotAvailable, OrgIdNotAllowedError, SeatRequiredError, OrgLimitReachedError, KeyLimitReachedError, BillingNotEnabledError, SeatLimitReachedError, RateLimitExceeded, UsageLimitExceeded, ) ``` ## Hierarchy ``` MemsyError ├── MemsyConnectionError # network error / timeout └── MemsyAPIError # non-2xx HTTP response ├── AuthenticationError # 401 — invalid or missing API key ├── AuthorizationError # 403 — key lacks required scope ├── FeatureNotAvailable # 403 — feature not on your tier ├── OrgIdNotAllowedError # 400 — free-tier: org_id not allowed in body ├── SeatRequiredError # 403 — endpoint requires an assigned seat ├── OrgLimitReachedError # 403 — tier limit on orgs reached ├── KeyLimitReachedError # 403 — tier limit on API keys reached ├── BillingNotEnabledError # 403 — billing not enabled for this org ├── SeatLimitReachedError # 409 — purchased seat limit reached ├── RateLimitExceeded # 429 — rate limit hit (after retries) └── UsageLimitExceeded # 429 — plan quota exceeded ``` ## `MemsyError` Base class. Catch this to handle any SDK error. ## `MemsyConnectionError` Raised when the SDK cannot reach the Memsy endpoint: connection refused, DNS failure, timeout. No additional attributes. ## `MemsyAPIError` Raised on any non-2xx response not captured by a specific subclass. | Attribute | Type | Description | |---|---|---| | `status_code` | `int` | HTTP status. | | `detail` | `str` | Server-provided detail message. | | `error_code` | `str \| None` | Stable machine-readable error code. | | `response` | `httpx.Response` | Raw response. | ## `AuthenticationError` (401) Invalid or missing API key. Same attributes as `MemsyAPIError`. ## `AuthorizationError` (403) API key is valid but lacks the required scope. | Extra attribute | Type | Description | |---|---|---| | `required_scope` | `str \| None` | Scope that was missing. | ## `FeatureNotAvailable` (403) The endpoint is gated behind a higher plan tier. | Extra attribute | Type | Description | |---|---|---| | `feature` | `str \| None` | Feature identifier. | | `current_tier` | `str \| None` | Plan tier the key is on. | | `upgrade_url` | `str \| None` | Deep link to the upgrade flow. | ## `OrgIdNotAllowedError` (400) Raised when a free-tier client sends an `org_id` field in the request body. Organization is derived from the API key on free-tier plans; passing one explicitly is rejected. No extra attributes beyond `MemsyAPIError`. ## `SeatRequiredError` (403) The console or control-plane endpoint requires the caller to have an assigned seat. Check your org's seat assignments in the dashboard. No extra attributes beyond `MemsyAPIError`. ## `OrgLimitReachedError` (403) Your tier's limit on org customization records has been reached. Raised by `client.orgs.create()` when the tier cap is exceeded. | Extra attribute | Type | Description | |---|---|---| | `limit` | `int \| None` | Org limit for your tier. | | `current` | `int \| None` | Current org count. | ## `KeyLimitReachedError` (403) Your tier's limit on API keys has been reached. | Extra attribute | Type | Description | |---|---|---| | `limit` | `int \| None` | Key limit for your tier. | | `current` | `int \| None` | Current active key count. | ## `BillingNotEnabledError` (403) A billing endpoint was called but billing has not been enabled for this org. The response includes a path to express interest in the Pro plan. | Extra attribute | Type | Description | |---|---|---| | `interest_path` | `str \| None` | API path to express interest (`/interest/express`). | ## `SeatLimitReachedError` (409) A seat-management operation would exceed the number of purchased seats. | Extra attribute | Type | Description | |---|---|---| | `purchased_seats` | `int \| None` | Total purchased seats. | | `assigned_seats` | `int \| None` | Currently assigned seats. | | `pending_invites` | `int \| None` | Pending invite count. | ## `RateLimitExceeded` (429) The SDK exhausted its automatic retries. | Extra attribute | Type | Description | |---|---|---| | `retry_after` | `float \| None` | Seconds to wait, parsed from `Retry-After`. | ## `UsageLimitExceeded` (429) A plan-level quota was exceeded. | Extra attribute | Type | Description | |---|---|---| | `dimension` | `str \| None` | Which quota was exhausted (e.g. `"events_ingested"`). | | `current` | `int \| None` | Current usage. | | `limit` | `int \| None` | Limit that was exceeded. | | `upgrade_url` | `str \| None` | Deep link. | ## See also - **[Error handling guide](/docs/error-handling)** — patterns for each exception. --- ## MemsyClient **URL:** https://docs.memsy.io/docs/reference/python/memsy-client **Description:** Synchronous HTTP client — constructor, methods, parameters, exceptions. Synchronous HTTP client for the Memsy API. ```python from memsy import MemsyClient ``` ## Constructor ```python MemsyClient( base_url: str, api_key: str, timeout: float = 30.0, max_retries: int = 3, retry_backoff: float = 1.0, ) ``` | Parameter | Type | Default | Description | |---|---|---|---| | `base_url` | `str` | — | Memsy API base URL, e.g. `"https://api.memsy.io/v1"`. Trailing slashes are trimmed. | | `api_key` | `str` | — | API key of the form `msy_...`. Sent as `Authorization: Bearer `. | | `timeout` | `float` | `30.0` | Request timeout in seconds. Covers connect + read + write. | | `max_retries` | `int` | `3` | Max automatic retries on HTTP 429. | | `retry_backoff` | `float` | `1.0` | Base exponential backoff in seconds between 429 retries. | ## Context manager ```python with MemsyClient(base_url="...", api_key="***") as client: client.health() ``` ## Methods ### `ingest(events)` Ingest a batch of events. ```python ingest(events: list[EventPayload]) -> IngestResponse ``` | Parameter | Type | Description | |---|---|---| | `events` | `list[EventPayload]` | Events to ingest. Same order is preserved in the response. | **Returns**: `IngestResponse` with `event_ids`, plus optional `usage` / `rate_limit`. **Raises**: `AuthenticationError`, `AuthorizationError`, `FeatureNotAvailable`, `RateLimitExceeded`, `UsageLimitExceeded`, `MemsyConnectionError`, `MemsyAPIError`. --- ### `search(query, *, actor_id, limit, threshold, include_source_events)` Retrieve memories via semantic search. ```python search( query: str, *, actor_id: str | None = None, limit: int = 10, threshold: float = 0.0, include_source_events: bool = False, ) -> SearchResponse ``` | Parameter | Type | Default | Description | |---|---|---|---| | `query` | `str` | — | Natural-language query. | | `actor_id` | `str \| None` | `None` | Filter results to a single actor. | | `limit` | `int` | `10` | Maximum number of results. | | `threshold` | `float` | `0.0` | Minimum similarity score. `0.0` = no filter. See [Threshold guidance](/docs/searching-memory#threshold). | | `include_source_events` | `bool` | `False` | Include source events in each result. | **Returns**: `SearchResponse` with `results: list[SearchResult]`, plus optional `usage` / `rate_limit`. --- ### `status(event_ids)` ```python status(event_ids: list[str]) -> StatusResponse ``` | Parameter | Type | Description | |---|---|---| | `event_ids` | `list[str]` | Event IDs returned from a prior `ingest()` call. | **Returns**: `StatusResponse` with `completed_ids`, `failed_ids`, `pending_ids`, `total`. --- ### `health()` ```python health() -> HealthResponse ``` **Returns**: `HealthResponse` with `status`, `version`, and optionally `billing_enabled` and `components`. --- ### `clear(container_tag)` ```python clear(container_tag: str) -> ClearResponse ``` | Parameter | Type | Description | |---|---|---| | `container_tag` | `str` | Container/conversation tag to clear. | **Returns** (when implemented): `ClearResponse` with `deleted` (count of cleared items). --- ### `close()` Close the underlying HTTP connection pool. Called automatically by the context manager. ```python close() -> None ``` --- ## Sub-resource accessors `MemsyClient` exposes three onboarding sub-resources and a console-memory sub-resource. Each is a thin accessor that delegates to the same underlying HTTP client. ### `client.orgs` CRUD for org customization records. See **[Onboarding](/docs/onboarding)**. ```python client.orgs.list() -> list[OrgResource] client.orgs.create(org_id, name, focus) -> OrgResource client.orgs.get(org_id) -> OrgResource client.orgs.update(org_id, *, name=None, focus=None, promotion_prompt=None) -> OrgResource client.orgs.regenerate_prompt(org_id) -> OrgResource client.orgs.delete(org_id) -> None ``` ### `client.roles` CRUD for role customization records within an org. The server assigns `role_id` — it is returned in the response, not supplied at creation. ```python client.roles.list(org_id) -> list[RoleResource] client.roles.create(org_id, name, focus) -> RoleResource client.roles.get(role_id, org_id) -> RoleResource client.roles.update(role_id, org_id, *, name=None, focus=None, promotion_prompt=None) -> RoleResource client.roles.regenerate_prompt(role_id, org_id) -> RoleResource client.roles.delete(role_id, org_id) -> None ``` ### `client.teams` CRUD for team customization records within an org. The server assigns `team_id` — it is returned in the response, not supplied at creation. ```python client.teams.list(org_id) -> list[TeamResource] client.teams.create(org_id, name, focus) -> TeamResource client.teams.get(team_id, org_id) -> TeamResource client.teams.update(team_id, org_id, *, name=None, focus=None, promotion_prompt=None) -> TeamResource client.teams.regenerate_prompt(team_id, org_id) -> TeamResource client.teams.delete(team_id, org_id) -> None ``` ### `client.memories` Browse and inspect stored memories. See **[Console Memories](/docs/console-memories)**. ```python client.memories.list( *, actor_id: str | None = None, # filter to a single actor's memories kind: str | None = None, type: str | None = None, status: str | None = None, sort: str = "observed_at_desc", search: str | None = None, limit: int = 50, offset: int = 0, ) -> MemoryListResponse client.memories.get(memory_id: str) -> MemoryItemResource client.memories.stats() -> MemoryStatsResponse ``` Pagination is stable across pages: every sort includes `memory_id` as a secondary tiebreaker, so walking `offset` won't return duplicates or skip records. --- ## Thread safety A `MemsyClient` wraps an `httpx.Client`, which is safe to share across threads for HTTP calls. Create one client per process and reuse it. ## See also - **[`AsyncMemsyClient`](/docs/reference/python/async-memsy-client)** — same interface, async. - **[`MemsyControlClient`](/docs/reference/python/control-client)** — control-plane client (billing, keys, usage). - **[Models](/docs/reference/python/models)** — every request/response dataclass. - **[Exceptions](/docs/reference/python/exceptions)** — every error class. --- ## Models **URL:** https://docs.memsy.io/docs/reference/python/models **Description:** Request and response dataclasses exposed by the SDK. All SDK models are plain Python `@dataclass`es imported from `memsy`. ## Core imports ```python from memsy import ( # Request EventPayload, # Core responses IngestResponse, SearchResponse, SearchResult, SourceEvent, StatusResponse, HealthResponse, ClearResponse, # Onboarding OrgResource, RoleResource, TeamResource, # Console memories MemoryScopeInfo, MemoryItemResource, MemoryListResponse, MemoryStatsResponse, # Control-plane (api/) MeResponse, UsageSummaryResponse, DimensionUsage, UsageTimeseriesResponse, TimeseriesPoint, BillingSummary, PaymentMethod, UpcomingInvoice, Invoice, ApiKeyInfo, ApiKeyListResponse, CreateKeyResponse, EventItemResponse, EventListResponse, ProInterestResponse, # Observability UsageInfo, RateLimitInfo, ) ``` --- ## Request models ### `EventPayload` A single event to ingest. ```python @dataclass class EventPayload: actor_id: str session_id: str kind: str # "user_message" | "assistant_message" | "tool_result" | "app_event" content: str ts: str | None = None # optional ISO-8601 timestamp metadata: str | None = None # optional JSON-serialised string role_id: str | None = None # role scope for the event team_id: str | None = None # team scope for the event ``` `role_id` and `team_id` are optional scope hints that associate the event with a specific role or team in your onboarding hierarchy. --- ## Core response models ### `IngestResponse` ```python @dataclass class IngestResponse: event_ids: list[str] usage: UsageInfo | None = None rate_limit: RateLimitInfo | None = None ``` ### `SearchResponse` ```python @dataclass class SearchResponse: results: list[SearchResult] usage: UsageInfo | None = None rate_limit: RateLimitInfo | None = None ``` ### `SearchResult` A single memory returned by `search()`. ```python @dataclass class SearchResult: id: str content: str score: float metadata: dict[str, Any] | None = None ``` **Typed metadata properties** (shortcuts into `metadata`): | Property | Type | Description | |---|---|---| | `.title` | `str \| None` | Memory title, if set. | | `.summary` | `str \| None` | Short summary of the memory. | | `.tags` | `list[str]` | Tags attached to this memory. | | `.entities` | `list[dict]` | Named entities extracted during processing. | | `.kind` | `str \| None` | Memory kind (e.g. `"semantic"`, `"episodic"`). | | `.type` | `str \| None` | Memory type (e.g. `"preference"`, `"fact"`). | | `.strength` | `float \| None` | Reinforcement strength (0–5.0). Not a probability. | | `.confidence` | `float \| None` | Extraction confidence (0–1). | | `.observed_at` | `str \| None` | ISO-8601 timestamp when memory was observed. | | `.source_event_ids` | `list[str]` | IDs of the source events. | | `.source_events` | `list[SourceEvent]` | Hydrated source events (requires `include_source_events=True`). | | `.source_metadata` | `list[dict]` | User-supplied metadata from source event(s). Each entry is `{event_id, metadata: dict}` for JSON-object payloads or `{event_id, raw: str}` for non-JSON. Capped at 5. | ### `SourceEvent` A source event hydrated inside `SearchResult.source_events`. ```python @dataclass class SourceEvent: event_id: str kind: str content: str ts: str | None = None ``` Only populated when you pass `include_source_events=True` to `search()`. ### `StatusResponse` ```python @dataclass class StatusResponse: completed_ids: list[str] failed_ids: list[str] pending_ids: list[str] total: int statuses: dict[str, str] | None = None # event_id → status map usage: UsageInfo | None = None rate_limit: RateLimitInfo | None = None ``` ### `HealthResponse` ```python @dataclass class HealthResponse: status: str version: str = "" billing_enabled: bool | None = None # present on control-plane /health components: dict[str, str] | None = None # component → status map usage: UsageInfo | None = None rate_limit: RateLimitInfo | None = None ``` ### `ClearResponse` ```python @dataclass class ClearResponse: deleted: int usage: UsageInfo | None = None rate_limit: RateLimitInfo | None = None ``` --- ## Onboarding models Returned by `client.orgs.*`, `client.roles.*`, and `client.teams.*`. ### `OrgResource` ```python @dataclass class OrgResource: org_id: str name: str focus: str promotion_prompt: str created_at: str updated_at: str prompt_meta: dict[str, Any] | None = None ``` ### `RoleResource` ```python @dataclass class RoleResource: role_id: str org_id: str name: str focus: str promotion_prompt: str created_at: str updated_at: str prompt_meta: dict[str, Any] | None = None ``` ### `TeamResource` ```python @dataclass class TeamResource: team_id: str org_id: str name: str focus: str promotion_prompt: str created_at: str updated_at: str prompt_meta: dict[str, Any] | None = None ``` --- ## Console memory models Returned by `client.memories.*`. ### `MemoryScopeInfo` ```python @dataclass class MemoryScopeInfo: level: str # "org" | "role" | "team" | "actor" actor_id: str | None = None team_id: str | None = None role_id: str | None = None ``` ### `MemoryItemResource` A full memory record as stored in the engine. ```python @dataclass class MemoryItemResource: memory_id: str org_id: str scope: MemoryScopeInfo type: str kind: str memory_kind: str status: str text: str confidence: float strength: float # bounded 0.0–5.0 by platform policy recall_count: int decay_half_life_days: float pinned: bool tags: list[str] entity_refs: list[dict[str, str]] source_event_ids: list[str] source_urls: list[str] summary: str | None = None payload: dict[str, Any] | None = None last_recalled_at: str | None = None effective_from: str | None = None effective_to: str | None = None observed_at: str | None = None created_at: str | None = None updated_at: str | None = None ``` ### `MemoryListResponse` ```python @dataclass class MemoryListResponse: items: list[MemoryItemResource] total: int limit: int offset: int ``` ### `MemoryStatsResponse` ```python @dataclass class MemoryStatsResponse: total: int total_memories: int active_memories: int by_type: dict[str, int] by_kind: dict[str, int] by_status: dict[str, int] avg_confidence: float avg_strength: float top_entities: list[dict[str, Any]] confidence_buckets: list[dict[str, Any]] | None = None date_range: dict[str, str | None] | None = None ``` --- ## Control-plane models (api/) Returned by `MemsyControlClient` and its sub-resources. ### `MeResponse` ```python @dataclass class MeResponse: customer_id: str email: str tier: str is_superadmin: bool org_id: str is_billing_admin: bool user_id: str | None = None org_role: str | None = None ``` ### `UsageSummaryResponse` / `DimensionUsage` ```python @dataclass class DimensionUsage: dimension: str used: int limit: int | None = None overage_rate: float | None = None @dataclass class UsageSummaryResponse: org_id: str tier: str period_start: str period_end: str dimensions: list[DimensionUsage] ``` ### `UsageTimeseriesResponse` / `TimeseriesPoint` ```python @dataclass class TimeseriesPoint: date: str dimension: str quantity: int @dataclass class UsageTimeseriesResponse: org_id: str granularity: str data: list[TimeseriesPoint] ``` ### `BillingSummary` / `PaymentMethod` / `UpcomingInvoice` ```python @dataclass class PaymentMethod: brand: str last4: str exp_month: int exp_year: int @dataclass class UpcomingInvoice: amount_due: int # amount in smallest currency unit (e.g. cents) currency: str period_end: int # Unix timestamp from Stripe @dataclass class BillingSummary: tier: str purchased_seats: int assigned_seats: int available_seats: int stripe_customer_id: str | None = None payment_method: PaymentMethod | None = None upcoming_invoice: UpcomingInvoice | None = None subscription_status: str | None = None billing_contact: str | None = None stripe_subscription_id: str | None = None ``` ### `Invoice` ```python @dataclass class Invoice: id: str amount_due: int amount_paid: int currency: str status: str created: str # stringified Unix timestamp as returned by the API hosted_invoice_url: str | None = None ``` ### `ApiKeyInfo` / `ApiKeyListResponse` / `CreateKeyResponse` ```python @dataclass class ApiKeyInfo: key_id: str prefix: str name: str scopes: list[str] is_active: bool created_at: str last_used_at: str | None = None expires_at: str | None = None @dataclass class ApiKeyListResponse: keys: list[ApiKeyInfo] max_keys: int active_count: int @dataclass class CreateKeyResponse: key_id: str raw_key: str # shown once — store securely prefix: str name: str scopes: list[str] ``` ### `EventItemResponse` / `EventListResponse` ```python @dataclass class EventItemResponse: event_id: str org_id: str actor_id: str kind: str content: str ts: str session_id: str | None = None metadata: dict[str, Any] | None = None ingested_at: str | None = None @dataclass class EventListResponse: items: list[EventItemResponse] total: int limit: int offset: int ``` ### `ProInterestResponse` ```python @dataclass class ProInterestResponse: message: str ``` --- ## Observability models ### `UsageInfo` Parsed from `X-Usage-*` and `X-Plan` response headers. Present on most `MemsyClient` responses. ```python @dataclass class UsageInfo: api_calls: int | None = None api_calls_limit: int | None = None events_ingested: int | None = None events_ingested_limit: int | None = None memory_stored: int | None = None memory_stored_limit: int | None = None llm_tokens: int | None = None llm_tokens_limit: int | None = None search_queries: int | None = None search_queries_limit: int | None = None plan: str | None = None ``` ### `RateLimitInfo` Parsed from `X-RateLimit-*` headers. ```python @dataclass class RateLimitInfo: limit: int | None = None remaining: int | None = None reset: int | None = None # Unix timestamp ``` See **[Usage and Rate Limits](/docs/usage-and-rate-limits)** for the full header mapping. --- ## Links - [GitHub](https://github.com/memsy-io/memsy) - [Support](mailto:cloudops@memsy.io)