diff --git a/apps/docs/docs.json b/apps/docs/docs.json
index 6be467ff7..a50b38200 100644
--- a/apps/docs/docs.json
+++ b/apps/docs/docs.json
@@ -104,7 +104,11 @@
{
"group": "Manage Content",
"icon": "folder-cog",
- "pages": ["document-operations", "memory-operations"]
+ "pages": [
+ "document-operations",
+ "memory-operations",
+ "memory-review"
+ ]
},
"overview/use-cases"
]
diff --git a/apps/docs/memory-operations.mdx b/apps/docs/memory-operations.mdx
index baf3b36ea..6f3906d51 100644
--- a/apps/docs/memory-operations.mdx
+++ b/apps/docs/memory-operations.mdx
@@ -201,6 +201,7 @@ Update a memory by creating a new version. The original is preserved with `isLat
## Next Steps
+- [Review Inferred Memories](/memory-review) — Approve or decline low-confidence memories
- [Document Operations](/document-operations) — Manage documents (SDK supported)
- [Search](/search) — Query your memories
- [Ingesting Content](/add-memories) — Add new content
diff --git a/apps/docs/memory-review.mdx b/apps/docs/memory-review.mdx
new file mode 100644
index 000000000..84fda2fa2
--- /dev/null
+++ b/apps/docs/memory-review.mdx
@@ -0,0 +1,288 @@
+---
+title: "Review Inferred Memories"
+sidebarTitle: "Memory Review"
+description: "List and act on low-confidence inferred memories — approve, decline, or undo"
+icon: "list-checks"
+---
+
+Supermemory's graph automatically **derives** new facts from patterns across your
+existing memories (see [Graph Memory](/concepts/graph-memory)). These derived facts
+are guesses — the engine wasn't told them directly — so they are flagged as
+**inferred** (`isInference: true`) and **down-weighted in search** until confirmed.
+
+These two endpoints let you build a review experience on top of that queue: list the
+inferred memories awaiting review, then **approve**, **decline**, or **undo** a
+decision on each one.
+
+
+These endpoints are scoped to a single [container tag](/concepts/container-tags)
+(space), under `/v3/container-tags/{containerTag}`.
+
+
+## How review affects ranking
+
+While a memory is unreviewed and inferred it is down-weighted in search, so the
+engine's guesses rank below facts you stated explicitly. Reviewing it resolves that
+either way:
+
+| Action | Result | Effect on search |
+|--------|--------|------------------|
+| **Approve** | `isInference` cleared | Ranks like a stated fact — no longer down-weighted |
+| **Decline** | `isForgotten` set | Removed from search entirely — a rejected guess is forgotten |
+| **Undo** | back to unreviewed | Returns to the queue; inferred and down-weighted again |
+
+A reviewed memory is stamped with `reviewStatus` in its metadata so it drops out of the
+review queue (declined memories also leave search, since they're forgotten). **Undo**
+clears that stamp — and un-forgets a declined memory — bringing it back.
+
+---
+
+## List Inferred Memories
+
+Return the inferred memories for a container tag that are still awaiting review (the
+review queue). Reviewed memories are excluded.
+
+```
+GET /v3/container-tags/{containerTag}/inferred
+```
+
+
+
+ ```typescript
+ const res = await fetch(
+ "https://api.supermemory.ai/v3/container-tags/user_123/inferred",
+ { headers: { "Authorization": `Bearer ${API_KEY}` } }
+ );
+
+ const { memories, total } = await res.json();
+ ```
+
+
+ ```bash
+ curl "https://api.supermemory.ai/v3/container-tags/user_123/inferred" \
+ -H "Authorization: Bearer $SUPERMEMORY_API_KEY"
+ ```
+
+
+
+### Path parameters
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `containerTag` | string | The container tag / space to read the review queue for |
+
+### Response
+
+```json
+{
+ "memories": [
+ {
+ "id": "mem_abc123",
+ "memory": "Alex likely works on Stripe's core payments product",
+ "parentCount": 3,
+ "createdAt": "2025-01-15T10:30:00.000Z",
+ "updatedAt": "2025-01-15T10:30:00.000Z",
+ "metadata": { "source": "derive" }
+ }
+ ],
+ "total": 1
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `memories[].id` | string | Memory entry ID — pass to the review endpoint |
+| `memories[].memory` | string | The inferred memory text |
+| `memories[].parentCount` | number | How many source memories this was derived from. Higher = stronger signal |
+| `memories[].createdAt` | string | ISO 8601 timestamp |
+| `memories[].updatedAt` | string | ISO 8601 timestamp |
+| `memories[].metadata` | object \| null | Arbitrary metadata stored on the memory |
+| `total` | number | Count of unreviewed inferred memories returned |
+
+
+The queue returns up to **50** memories, ordered by `parentCount` descending (most
+strongly supported first), then by `createdAt` descending. It excludes anything that
+is forgotten, expired, or already reviewed. An unknown or empty container tag returns
+`{ "memories": [], "total": 0 }`.
+
+
+---
+
+## Review an Inferred Memory
+
+Record a decision on a single inferred memory.
+
+```
+POST /v3/container-tags/{containerTag}/inferred/{memoryId}/review
+```
+
+
+
+ ```typescript
+ const res = await fetch(
+ "https://api.supermemory.ai/v3/container-tags/user_123/inferred/mem_abc123/review",
+ {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${API_KEY}`,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ action: "approve" })
+ }
+ );
+
+ const result = await res.json();
+ // { id: "mem_abc123", isInference: false, isForgotten: false, reviewStatus: "approved" }
+ ```
+
+
+ ```bash
+ curl -X POST \
+ "https://api.supermemory.ai/v3/container-tags/user_123/inferred/mem_abc123/review" \
+ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"action": "approve"}'
+ ```
+
+
+
+### Path parameters
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `containerTag` | string | The container tag / space the memory belongs to |
+| `memoryId` | string | The memory entry ID from the list endpoint |
+
+### Body parameters
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `action` | string | yes | One of `approve`, `decline`, or `undo` |
+
+
+The reject action is named **`decline`**. There is no `reject` value.
+
+
+**Action semantics:**
+
+- **`approve`** — Promote the memory: clears `isInference`, so it ranks like a stated
+ fact instead of a down-weighted guess. Stamps `reviewStatus: "approved"`.
+- **`decline`** — Reject the suggestion: the memory is **forgotten**
+ (`isForgotten: true`) and stamped `reviewStatus: "declined"`, so it leaves both
+ search and the review queue.
+- **`undo`** — Revert a prior `approve`/`decline` back to the unreviewed inferred
+ state: restores `isInference: true`, un-forgets the memory (`isForgotten: false`),
+ and clears the review stamp, so it returns to the queue.
+
+### Response
+
+```json
+{
+ "id": "mem_abc123",
+ "isInference": false,
+ "isForgotten": false,
+ "reviewStatus": "approved"
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | string | The reviewed memory ID |
+| `isInference` | boolean | `false` after approve; `true` after decline or undo |
+| `isForgotten` | boolean | `true` after decline (the memory is forgotten); `false` otherwise |
+| `reviewStatus` | `"approved"` \| `"declined"` \| `null` | The new status; `null` after an undo |
+
+### Errors
+
+| Status | When |
+|--------|------|
+| `401` | Missing or invalid authentication |
+| `404` | The container tag or memory was not found in your organization |
+| `409` | The memory isn't reviewable for this action — it's not an inferred memory, or there's no prior review to undo |
+
+---
+
+## Building a review experience
+
+The endpoints are designed for an optimistic, one-at-a-time review UI (swipe to keep /
+decline, with undo). A typical client fetches the queue once, then pops each card off
+locally as the user decides — `undo` re-adds it.
+
+
+```typescript
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+const BASE = "https://api.supermemory.ai/v3";
+const key = (tag: string) => ["inferred-memories", tag] as const;
+
+export type InferredMemory = {
+ id: string;
+ memory: string;
+ parentCount: number;
+ createdAt: string;
+ updatedAt: string;
+ metadata: Record | null;
+};
+
+export type ReviewAction = "approve" | "decline" | "undo";
+
+export function useInferredMemories(containerTag: string) {
+ return useQuery({
+ queryKey: key(containerTag),
+ queryFn: async (): Promise => {
+ const res = await fetch(`${BASE}/container-tags/${containerTag}/inferred`, {
+ headers: { Authorization: `Bearer ${API_KEY}` },
+ });
+ if (!res.ok) throw new Error("Failed to load review queue");
+ const data = await res.json();
+ return data.memories ?? [];
+ },
+ staleTime: 60_000,
+ });
+}
+
+export function useReviewInferredMemory(containerTag: string) {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (vars: { memoryId: string; action: ReviewAction }) => {
+ const res = await fetch(
+ `${BASE}/container-tags/${containerTag}/inferred/${vars.memoryId}/review`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${API_KEY}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ action: vars.action }),
+ },
+ );
+ if (!res.ok) throw new Error("Review failed");
+ return res.json();
+ },
+ onSuccess: (_data, { memoryId, action }) => {
+ // approve/decline remove the card; undo brings it back, so refetch.
+ if (action === "undo") {
+ queryClient.invalidateQueries({ queryKey: key(containerTag) });
+ return;
+ }
+ queryClient.setQueryData(key(containerTag), (prev) =>
+ prev?.filter((m) => m.id !== memoryId),
+ );
+ },
+ });
+}
+```
+
+
+
+There is no separate "skip" action. A swipe-to-skip is purely client-side — don't send
+a request and the memory simply stays in the queue for a later session.
+
+
+---
+
+## Next Steps
+
+- [Graph Memory](/concepts/graph-memory) — How inferred (`derive`) memories are created
+- [Memory Operations](/memory-operations) — Create, forget, and update memories
+- [Search](/search) — How inferred memories are ranked in results
diff --git a/apps/docs/user-profiles.mdx b/apps/docs/user-profiles.mdx
index e2b3ae461..cb5c7cd18 100644
--- a/apps/docs/user-profiles.mdx
+++ b/apps/docs/user-profiles.mdx
@@ -121,6 +121,9 @@ Get profile and search results in one call by adding the `q` parameter:
| `containerTag` | string | Yes | User/project identifier |
| `q` | string | No | Search query (includes search results in response) |
| `threshold` | 0-1 | No | Filter search results by relevance score |
+| `filters` | object | No | Metadata filters applied to profile and search results |
+| `include` | string[] | No | Sections to return — any of `"static"`, `"dynamic"`, `"buckets"`. Omit to return all |
+| `buckets` | string[] | No | Restrict the `buckets` section to specific keys. Omit for all configured buckets |
---
@@ -180,6 +183,142 @@ ${result.searchResults?.results.map(m => m.memory).join('\n') || 'None'}
---
+## Profile Buckets
+
+Buckets are **custom topical categories** for a profile — an axis that sits alongside
+`static` and `dynamic`. Where static/dynamic split facts by how long-lived they are,
+buckets group them by subject (e.g. `preferences`, `goals`, `work`). As content is
+ingested, a classifier assigns each memory to the buckets it matches, so you can pull
+just the slice of context a given surface needs.
+
+Every org starts with a built-in `preferences` bucket. You can define your own at the
+organization or space level in your console settings; space-level buckets are
+**add-only** — a container tag inherits all org buckets and may add more, but cannot
+disable them.
+
+### Requesting buckets
+
+Pass `include: ["buckets"]` to return bucket-organized memories, and optionally
+`buckets` to limit the response to specific keys. `include` also lets you skip
+sections you don't need — `["buckets"]` alone omits `static` and `dynamic`.
+
+
+
+ ```typescript
+ const res = await fetch("https://api.supermemory.ai/v4/profile", {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${API_KEY}`,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ containerTag: "user_123",
+ include: ["buckets"],
+ buckets: ["preferences", "goals"] // optional — omit for all buckets
+ })
+ });
+
+ const { profile } = await res.json();
+ console.log(profile.buckets.preferences);
+ console.log(profile.buckets.goals);
+ ```
+
+
+ ```bash
+ curl -X POST "https://api.supermemory.ai/v4/profile" \
+ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "containerTag": "user_123",
+ "include": ["buckets"],
+ "buckets": ["preferences", "goals"]
+ }'
+ ```
+
+
+
+**Response:**
+```json
+{
+ "profile": {
+ "buckets": {
+ "preferences": [
+ "[Summary] Prefers concise, technical answers and dark-mode tooling",
+ "[Recent] Switched their editor to Zed"
+ ],
+ "goals": [
+ "[Recent] Wants to ship the billing revamp this quarter"
+ ]
+ }
+ }
+}
+```
+
+
+**`[Recent]` and `[Summary]` labels.** To keep profiles dense, an entity's older
+memories are periodically aggregated into a short synthesis. Entries prefixed
+`[Summary]` are that aggregated context; entries prefixed `[Recent]` were ingested
+since the last aggregation and aren't summarized yet. The `dynamic` section uses the
+same `[Recent]` prefix (plus a `[YYYY-MM-DD]` date). Strip the prefixes if you only
+want raw text, or keep them to signal recency to your model.
+
+
+### List bucket definitions
+
+To see which buckets are configured for a container tag (org buckets merged with any
+space-level additions), call `/v4/profile/buckets`:
+
+
+
+ ```typescript
+ const res = await fetch("https://api.supermemory.ai/v4/profile/buckets", {
+ method: "POST",
+ headers: {
+ "Authorization": `Bearer ${API_KEY}`,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({ containerTag: "user_123" })
+ });
+
+ const { buckets } = await res.json();
+ // [{ key: "preferences", description: "..." }, ...]
+ ```
+
+
+ ```bash
+ curl -X POST "https://api.supermemory.ai/v4/profile/buckets" \
+ -H "Authorization: Bearer $SUPERMEMORY_API_KEY" \
+ -H "Content-Type: application/json" \
+ -d '{"containerTag": "user_123"}'
+ ```
+
+
+
+**Response:**
+```json
+{
+ "buckets": [
+ {
+ "key": "preferences",
+ "description": "Explicit first-person preferences the person directly stated."
+ }
+ ]
+}
+```
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `buckets[].key` | string | Stable slug, also stored on each memory. Lowercase alphanumeric with `-`/`_`, 1–64 chars |
+| `buckets[].description` | string | What belongs in the bucket — guides the ingestion classifier |
+
+
+Bucket descriptions steer classification. A precise description ("Explicit
+first-person preferences only — exclude inferred traits") yields cleaner buckets than
+a vague one. `static` and `dynamic` are reserved and can't be used as bucket keys.
+
+
+---
+
## Framework Examples
@@ -249,8 +388,9 @@ ${result.searchResults?.results.map(m => m.memory).join('\n') || 'None'}
```typescript
interface ProfileResponse {
profile: {
- static: string[]; // Long-term facts
- dynamic: string[]; // Recent context
+ static?: string[]; // Long-term facts
+ dynamic?: string[]; // Recent context
+ buckets?: Record; // Topical buckets, keyed by bucket key
};
searchResults?: { // Only if q parameter provided
results: SearchResult[];