# Understanding the Layered Architecture of notebooklm-py: CLI → Client → Core → RPC

> Explore the layered architecture of notebooklm-py CLI Client Core RPC. Understand how this Python library separates concerns for a robust Google NotebookLM interface. Learn more.

- Repository: [Teng Lin/notebooklm-py](https://github.com/teng-lin/notebooklm-py)
- Tags: architecture
- Published: 2026-03-09

---

**The notebooklm-py library organizes its codebase into four distinct layers—CLI, Client, Core, and RPC—that separate user interaction, high-level API operations, HTTP transport management, and raw protocol serialization to provide a robust Python interface for Google NotebookLM.**

notebooklm-py is an open-source Python client for Google NotebookLM that enables both command-line usage and programmatic automation. Understanding this **layered architecture of notebooklm-py** is essential for developers who want to debug network issues, extend the library's capabilities, or integrate NotebookLM functionality into larger applications.

## The Four-Layer Stack

The codebase follows a strict separation of concerns where each layer has a single responsibility and communicates only with the layer immediately below it.

### CLI Layer: Command-Line Interface

The CLI layer handles argument parsing and user interaction using the Click framework. Located in [[`src/notebooklm/notebooklm_cli.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/notebooklm_cli.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/notebooklm_cli.py), this layer remains intentionally thin—it parses commands like `notebooklm list` or `notebooklm ask`, instantiates the client within an async context, and formats output for the terminal.

When a user runs a command, the CLI creates a `NotebookLMClient` via `NotebookLMClient.from_storage()` inside an `async with` block, delegates the operation to the Client layer, and prints the results.

### Client Layer: High-Level Python API

The Client layer provides the primary public API surface through the `NotebookLMClient` class in [[`src/notebooklm/client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py). This layer exposes namespaced sub-APIs such as `client.notebooks`, `client.sources`, `client.artifacts`, and `client.chat`, offering intuitive methods like `list()`, `add_url()`, and `ask()`.

Rather than handling HTTP details directly, the Client holds a reference to a `ClientCore` instance and delegates all operations to it. For example, when `client.notebooks.list()` is called, the underlying `NotebooksAPI` class invokes `self._core.rpc_call()` with the appropriate parameters.

### Core Layer: HTTP Session and Request Management

The Core layer in [[`src/notebooklm/_core.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py` manages the heavy lifting of network communication. The `ClientCore` class maintains an HTTP session, constructs the `batchexecute` URLs required by Google's internal API, handles authentication token refresh, manages retries, and caches conversation state.

This layer shields the Client from transport concerns. When `rpc_call()` is invoked, Core builds the full request payload, attaches CSRF tokens, executes the POST request, and handles error mapping before returning native Python structures to the caller.

### RPC Layer: Protocol Definitions and Serialization

The RPC layer defines the obfuscated method identifiers and serialization logic required to communicate with Google's internal NotebookLM endpoints. Located primarily in [[`src/notebooklm/rpc/types.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/types.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/types.py`), this layer exports the `RPCMethod` enum containing constants like `RPCMethod.LIST_NOTEBOOKS` (mapped to the obfuscated ID `"wXbhsf"`).

Supporting files [[`src/notebooklm/rpc/encoder.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/encoder.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/encoder.py) and [[`src/notebooklm/rpc/decoder.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/decoder.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/decoder.py) handle the transformation between Python dictionaries and the batchexecute payload format. This isolation makes it straightforward to update method IDs when Google changes their internal API surface.

## Request Flow Through the Architecture

A typical operation—listing notebooks—flows through all four layers sequentially:

1. The **CLI** receives the `notebooklm list` command and creates a `NotebookLMClient` instance using `from_storage()`.
2. The **Client** layer calls `client.notebooks.list()`, which delegates to `NotebooksAPI`.
3. The **Core** layer receives the call via `ClientCore.rpc_call(RPCMethod.LIST_NOTEBOOKS, params)`, builds the HTTP request with proper authentication, and sends it to Google's servers.
4. The **RPC** layer supplies the method ID `"wXbhsf"` and decodes the JSON response back into Python objects.
5. The result propagates back up to the CLI for display.

## Practical Usage Examples

### Using the Python Client API

Interact with the Client layer directly to build automated workflows:

```python
import asyncio
from notebooklm import NotebookLMClient

async def workflow():
    # Initialize client from stored authentication

    async with await NotebookLMClient.from_storage() as client:
        # Client layer → Core layer → RPC layer

        notebooks = await client.notebooks.list()
        print(f"Found {len(notebooks)} notebooks")
        
        # Add a source to the first notebook

        nb_id = notebooks[0]["id"]
        await client.sources.add_url(nb_id, "https://example.com/article")
        
        # Ask a question using the chat API

        answer = await client.chat.ask(nb_id, "Summarize the key points")
        print(f"Answer: {answer}")

asyncio.run(workflow())

```

### Using the CLI Interface

For manual operations, the CLI layer provides the same functionality through terminal commands:

```bash

# Authenticate once to store session cookies

notebooklm login

# List all notebooks (CLI → Client → Core → RPC)

notebooklm list

# Select a notebook for subsequent commands

notebooklm use abc123

# Ask a question through the chat interface

notebooklm ask "What are the main takeaways?"

```

## Summary

- **CLI Layer** ([[`notebooklm_cli.py`](https://github.com/teng-lin/notebooklm-py/blob/main/notebooklm_cli.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/notebooklm_cli.py)): Parses arguments with Click and orchestrates client calls.
- **Client Layer** ([[`client.py`](https://github.com/teng-lin/notebooklm-py/blob/main/client.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/client.py)): Exposes the public `NotebookLMClient` API with namespaced sub-resources.
- **Core Layer** ([[`_core.py`](https://github.com/teng-lin/notebooklm-py/blob/main/_core.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py)): Manages HTTP sessions, authentication, retries, and `batchexecute` URL construction.
- **RPC Layer** ([[`rpc/types.py`](https://github.com/teng-lin/notebooklm-py/blob/main/rpc/types.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/types.py)): Defines obfuscated method IDs like `"wXbhsf"` and handles payload serialization.

## Frequently Asked Questions

### Why does notebooklm-py separate the Client and Core layers?

The separation isolates HTTP transport concerns (timeouts, token refresh, error handling in `ClientCore`) from the user-facing API surface. This allows the Client layer to remain stable and well-documented while the Core implementation can evolve to handle changes in Google's authentication mechanisms or endpoint structures.

### How do I extend functionality at the RPC layer?

Add new method IDs to the `RPCMethod` enum in [[`src/notebooklm/rpc/types.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/types.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/types.py), then implement corresponding encoder logic in [[`src/notebooklm/rpc/encoder.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/encoder.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/encoder.py) and decoder logic in [[`src/notebooklm/rpc/decoder.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/decoder.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/rpc/decoder.py). Finally, expose the operation through a new method in the appropriate Client sub-API class that calls `self._core.rpc_call()`.

### Can I use the Core layer directly without the Client?

Yes, you can instantiate `ClientCore` directly from [[`src/notebooklm/_core.py`](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py)](https://github.com/teng-lin/notebooklm-py/blob/main/src/notebooklm/_core.py) and call `rpc_call()` with raw `RPCMethod` enums and parameters. However, this bypasses the convenience methods and input validation provided by the Client layer, requiring you to handle parameter formatting and response parsing manually.

### What happens when Google changes their internal API method IDs?

Since all obfuscated method IDs are centralized in the RPC layer's [`types.py`](https://github.com/teng-lin/notebooklm-py/blob/main/types.py) file, updates only require changing the string constants in the `RPCMethod` enum (for example, updating `LIST_NOTEBOOKS = "wXbhsf"` to a new identifier). The Core layer will automatically use the updated constants for all subsequent `batchexecute` requests without requiring changes to the Client or CLI code.