SuperBooksDevelopers
SDKs

Python

The official superbooks PyPI package — sync and async clients, error handling, and retries.

The superbooks package wraps the MCP API in per-domain methods, so you call sb.transactions.list() instead of assembling JSON-RPC envelopes. It ships a synchronous client and an asynchronous one with the same surface.

Requires Python 3.10 or newer. Its only runtime dependency is httpx.

Install

pip install superbooks

Creating a client

import os

from superbooks import SuperBooks

sb = SuperBooks(api_key=os.environ["SUPERBOOKS_API_KEY"])

The full signature:

SuperBooks(
    api_key=None,
    base_url=None,
    *,
    timeout=60.0,
    max_retries=0,
    http_client=None,
)
  • api_key falls back to the SUPERBOOKS_API_KEY environment variable. If neither is set it raises ValueError immediately at construction, not on the first call. An explicit argument beats the environment variable.
  • base_url falls back to SUPERBOOKS_BASE_URL, then https://api.superbooks.io. Pass the root — the SDK appends /mcp itself. Trailing slashes are tolerated.
  • timeout is 60 seconds per request.
  • max_retries is 0. See Retries.
  • http_client takes your own httpx.Client for proxies or custom transports. You keep ownership: close() will not close a client you passed in.

Both clients are context managers — with SuperBooks() as sb: for the sync one, or call sb.close() yourself.

Calling tools

Each MCP tool maps to a method on its domain — strip the domain prefix, keep the rest snake_case:

recent = sb.transactions.list(
    from_="2026-01-01",
    to="2026-03-31",
    limit=50,
)

draft = sb.invoices.create_draft(
    customer_id="00000000-0000-0000-0000-000000000000",
    currency="USD",
    issue_date="2026-04-01",
    due_date="2026-04-30",
    line_items=[{"name": "Consulting", "quantity": 10, "price": 150}],
)

Two Python keyword collisions get a trailing underscore. The correct name still goes on the wire — only the Python spelling changes.

  • fromfrom_. This affects transactions.list, invoices.list, tracker.list_entries, and five reports methods, so sb.reports.burn_rate(from_="2026-01-01", to="2026-06-30").
  • globalglobal_, so the search_global tool is sb.search.global_("acme").

Nothing else in the 45 tools collides.

Across every method, required parameters come first and may be passed positionally; optional ones are keyword-only. String enums are typed as Literal. Parameters you leave unset are omitted from the request entirely rather than sent as null, so the server's own defaults still apply.

Every tool, with its full parameter list, is in the API reference.

Async

AsyncSuperBooks mirrors the synchronous client method for method — the only difference is that calls are awaited. It takes an httpx.AsyncClient for http_client.

import asyncio
import os

from superbooks import AsyncSuperBooks


async def main() -> None:
    async with AsyncSuperBooks(api_key=os.environ["SUPERBOOKS_API_KEY"]) as sb:
        recent = await sb.transactions.list(limit=50)
        print(recent)


asyncio.run(main())

If you would rather not nest, there is an explicit closer — but note it is aclose(), not close():

sb = AsyncSuperBooks()
try:
    ...
finally:
    await sb.aclose()

Async pays off when you fan out across independent calls. Mind the rate limit while doing it: concurrency makes 120 requests a minute much easier to reach.

What a call returns

Plain dicts. No dataclasses, no pydantic, no model layer — a deliberate choice to keep the dependency list at httpx alone.

Precisely: the tool's structuredContent when present, otherwise the raw MCP content blocks. Every tool in the current surface supplies structuredContent, so in practice you get a dict. It is typed as Any, because the payload shape belongs to the server.

The package ships py.typed, so your parameters are still checked by mypy and pyright even though return values are not.

Pagination

Manual — there is no auto-paginating iterator.

cursor = None

while True:
    page = sb.transactions.list(limit=100, cursor=cursor)
    for transaction in page["items"]:
        print(transaction["id"])
    cursor = page["nextCursor"]
    if cursor is None:
        break

Note the asymmetry: you pass cursor in and read nextCursor out, and nextCursor is None on the last page.

limit accepts 1–100 on every paginated tool, but the default varies by tool — 25 for transactions.list, invoices.list, customers.list and tracker.list_projects, 50 for tracker.list_entries, 30 for search.global_, and 10 for reports.top_customers. Pass it explicitly rather than relying on the default. Prefer large pages, since one call for 100 rows costs a hundredth of the budget a hundred single-row calls would.

Error handling

Every exception descends from SuperBooksError, so catching that one catches everything. All of them import from the top level:

from superbooks import RateLimitError, SuperBooksError

try:
    transactions = sb.transactions.list(limit=10)
except RateLimitError as error:
    print(f"Rate limited; retry in {error.retry_after}s")
    raise
except SuperBooksError as error:
    print(error)
    raise
SuperBooksError
├── APIError                 .status_code, .body
│   ├── AuthenticationError  401
│   ├── AuthorizationError   403
│   └── RateLimitError       429, adds .retry_after
├── ConnectionError          never reached the API (DNS/TCP/TLS/timeout)
├── ProtocolError            reply was not valid MCP/JSON-RPC
└── ToolError                tool ran and reported failure; .code, .data

Anything else non-2xx — 400, 500, 503 — raises a plain APIError. ToolError also covers isError: true tool results, carrying the tool's own text as the message. .body is truncated to roughly 2 KB.

Two naming details worth knowing. AuthorizationError is deliberately not called PermissionError, and this package's ConnectionError subclasses SuperBooksError rather than OSError — so neither can be confused with, or accidentally caught alongside, the Python builtins of those names.

There is also no request_id field: the API returns no correlation id, so do not build logging around one.

Retries

Off by default (max_retries=0). The reasoning is worth knowing: the API's Retry-After on a 429 is a full 60 seconds, so a silent automatic retry would park your thread for minutes. Raising is the more honest default.

Opt in with SuperBooks(max_retries=2). When enabled it retries 429 only — never 5xx, never connection errors — so a failed write is never silently replayed. Retry-After is honoured exactly, falling back to 1 second if the header is missing or unparseable, and each individual sleep is capped at 60 seconds. It is not exponential backoff; it is "do what the server said, bounded".

Escape hatches

Anything not yet wrapped in a release is still reachable:

sb.tools.list()
sb.tools.call("transactions_list", {"limit": 10})

tools.list() is filtered server-side by your credential's scopes, so a read-only key genuinely does not see write or destructive tools.

Using SuperBooks from an AI client

The SDK is for writing code. If you want Claude, Cursor, or another AI client to work with your SuperBooks data, you do not need this package at all — connect the client to the hosted endpoint directly:

claude mcp add --transport http superbooks https://api.superbooks.io/mcp \
  --header "Authorization: Bearer sb_your_api_key_here"

See Connecting AI clients for per-client setup.

On this page