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

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), 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). 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` 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`), 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) and [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:

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:


# 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

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), 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) and decoder logic in [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) 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →