> ## Documentation Index
> Fetch the complete documentation index at: https://docs.memory.anonalabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Delete a user's memories, provably

> A data-subject request answered end to end: find everything about one person, export it, remove it, show your work.

[← All use cases](https://docs.memory.anonalabs.com/use-cases/overview.md)

[support\_bot\_scoping.py](https://github.com/anonalabs/Anona-Memory-SDK/blob/main/examples/support_bot_scoping.py)[user\_profiles.py](https://github.com/anonalabs/Anona-Memory-SDK/blob/main/examples/user_profiles.py)

## The scenario

Someone emails asking what you hold about them, and then asks you to delete it.
You have a deadline measured in days, and "it is spread across a memory system"
is not an answer you can send.

The whole thing turns on one earlier decision: whether writes were scoped. If
every memory about a person carries their scope, this is a runbook. If not, it
is a manual search you cannot certify.

## Step 1 — Find everything about them

```python theme={null}
def everything_about(user_id):
    rows, offset = [], 0
    while True:
        page = client.list_memories(
            space_id="support",
            user_id=user_id,
            prefer_observations=False,  # the raw facts as well as the notes
            limit=200,
            offset=offset,
        )
        rows += page["items"]
        offset += len(page["items"])
        if not page["items"] or offset >= page["total"]:
            return rows

hits = everything_about("user-4417")
```

This is a list, not a search. `retrieve` ranks by relevance to a query and
stops at `limit`, which is the wrong tool for "everything" — and it has no
match-all query (`query="*"` is refused with `422`). `list_memories` pages the
whole scope and tells you the `total`, so you know when you have all of it.
`prefer_observations=False` matters: by default a note hides the raw facts it
was built from, and those facts are data about the person too.

A scoped read is strict — it matches only memories carrying that scope, and
never untagged ones. That strictness is exactly what you want here: the result
is defensible rather than best-effort.

## Step 2 — Give them a copy first

The subject is usually entitled to the data before it goes. `hits` from step 1
*is* the per-person copy — every row, with its content and dates — so write it
to a file before anything is deleted:

```python theme={null}
import json
with open("user-4417-export.json", "w") as f:
    json.dump(hits, f, indent=2)
```

If you also want the full-space bundle for your own records, take it from the
dashboard first — see [Export Your Memory](https://docs.memory.anonalabs.com/guides/export.md). Full-space export is
dashboard-only; there is no API-key endpoint for it.

## Step 3 — Delete, one by one, and keep the ids

```python theme={null}
from anona import AnonaError

removed, already_gone = [], []
for row in hits:
    try:
        client.delete_memory(space_id="support", memory_id=row["id"])
        removed.append(row["id"])
    except AnonaError as e:
        if e.code != "memory_not_found":
            raise
        already_gone.append(row["id"])

print(len(removed), "deleted,", len(already_gone), "removed with a fact they came from")
```

Deleting a fact also deletes every note built from it, so a note further down
`hits` can already be gone by the time the loop reaches it. That is a
`404 memory_not_found`, and it is success, not failure — keep it in its own
list rather than stopping the loop.

Keep both lists. The ids, with a timestamp, are what you attach to your
response to the request.

## Step 4 — Prove it

```python theme={null}
assert everything_about("user-4417") == []
```

Re-running the same scoped query and getting nothing is the evidence. Run it as
a test, not by eye.

## Evals

1. Write ten memories for one user and five for another.
2. Run the runbook for the first. Confirm every id step 1 found ends up in
   `removed` or `already_gone`, and none of them raised anything else.
3. Confirm the second user's five are untouched — the most important assertion
   here, and the one a manual process gets wrong.
4. Re-run the scoped list and assert empty.

## Guardrails

> [!WARNING]
>
> **Deletion is permanent and there is no undo.** Export first, every time, and
> have the exported bundle in hand before the first `delete_memory` call.

* **Unscoped memories are invisible to this process.** If you adopted scoping
  late, memories written before it carry no `user_id` and a scoped query will
  never find them — meaning your deletion is incomplete and you will not be
  told. Scope from day one, or backfill before you promise anything.
* **A shared space needs the same runbook run by its owner.** Deleting is
  owner-only; a visitor cannot erase data from a space they were invited to.
* **Consolidated observations are derived from raw facts, and they go with
  them.** Deleting a fact removes every observation built from it and
  re-consolidates that observation's other sources without it. Re-check with a
  scoped read afterwards anyway — that read, not the delete's status code, is
  the evidence you keep.
* **This page is a runbook, not legal advice.** What your obligations are, and
  within what deadline, depends on your jurisdiction and your role as controller
  or processor.

## Built from

Both are complete, runnable scripts:
[`support_bot_scoping.py`](https://github.com/anonalabs/Anona-Memory-SDK/blob/main/examples/support_bot_scoping.py),
[`user_profiles.py`](https://github.com/anonalabs/Anona-Memory-SDK/blob/main/examples/user_profiles.py).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.