# Notion

> Keep each user's Notion in their memory with their own sign-in to Notion. A changed page teaches only what changed, a page that is gone takes what it taught with it, and disconnecting forgets everything from Notion.

Keep each of your users' Notion in their memory. Your app already holds a Notion token for each user, from
their sign-in to your Notion integration; this sync reads the pages that token can see and adds each one under
its own id with the label `channel: notion`. Run it on a schedule. A page that hasn't changed costs nothing,
a changed page teaches only what changed, a page that is gone takes what it taught with it, and when the user
disconnects Notion, one call forgets everything that came from it.

## Install

```bash
pip install httpx geniffy
```

```bash
uv add httpx geniffy
```

Set `GENIFFY_API_KEY` from **API keys** in the Geniffy app.

## The sync

```python
import httpx
from geniffy import Geniffy

geniffy = Geniffy()                               # reads GENIFFY_API_KEY
LABELS = {"channel": "notion"}


def notion(token: str) -> httpx.Client:
    """One user's Notion, with the token their sign-in to your integration gave you."""
    return httpx.Client(base_url="https://api.notion.com/v1", timeout=30,
                        headers={"Authorization": f"Bearer {token}", "Notion-Version": "2026-03-11"})


def pages(api: httpx.Client):
    """Every page the user shared with your integration."""
    body: dict = {"filter": {"property": "object", "value": "page"}, "page_size": 100}
    while True:
        out = api.post("/search", json=body).raise_for_status().json()
        yield from out["results"]
        if not out.get("has_more"):
            return
        body["start_cursor"] = out["next_cursor"]


def title_of(page: dict) -> str:
    for prop in page.get("properties", {}).values():
        if prop.get("type") == "title":
            return "".join(t["plain_text"] for t in prop["title"]) or "Untitled"
    return "Untitled"


def text_of(api: httpx.Client, block_id: str, depth: int = 0) -> list[str]:
    """A page's words, block by block, with blocks inside blocks two levels down."""
    lines: list[str] = []
    params: dict = {"page_size": 100}
    while True:
        out = api.get(f"/blocks/{block_id}/children", params=params).raise_for_status().json()
        for block in out["results"]:
            line = "".join(t["plain_text"] for t in block.get(block["type"], {}).get("rich_text", [])).strip()
            if line:
                lines.append(line)
            if block.get("has_children") and depth < 2:
                lines += text_of(api, block["id"], depth + 1)
        if not out.get("has_more"):
            return lines
        params["start_cursor"] = out["next_cursor"]


def sync(user_id: str, token: str) -> int:
    """Bring one user's Notion into their memory. Returns how many pages it holds."""
    mem = geniffy.space(f"user_{user_id}")
    seen = set()
    with notion(token) as api:
        for page in pages(api):
            text = "\n\n".join(text_of(api, page["id"]))
            if text:
                mem.memories.add(text, title=title_of(page), external_id=f"notion:{page['id']}", labels=LABELS)
                seen.add(f"notion:{page['id']}")
    mem.sources.delete_labelled(LABELS, keep=seen)    # the rest: deleted in Notion, or no longer shared with you
    return len(seen)


def disconnect(user_id: str) -> int:
    """The user disconnected Notion: everything that came from it goes. Returns how many pages."""
    return geniffy.space(f"user_{user_id}").sources.delete_labelled(LABELS)
```

## How it behaves

- **Each page is one source,** under its Notion id. Sent again unchanged, it costs nothing; changed, only the
  paragraphs that changed are learned, and what was removed is taken back. See
  [Your own ids](https://docs.geniffy.com/add-memories/your-own-ids).
- **A page that is gone goes from memory too,** with what it taught, whether it was deleted in Notion or the
  user stopped sharing it with your integration.
- **Recall can keep to Notion,** or leave it out: `mem.context(question, labels={"channel": "notion"})`. See
  [Labels](https://docs.geniffy.com/add-memories/labels).
- **Disconnecting forgets it all** in one call, and nothing the user added another way.

The same shape works for any source your users connect: list what the token can see, add each item under its
own id with a label for the source, delete what is gone with `delete_labelled(labels, keep=...)`, and delete by
the label when they disconnect.

Source: https://docs.geniffy.com/integrations/notion
