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 superbooksCreating 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_keyfalls back to theSUPERBOOKS_API_KEYenvironment variable. If neither is set it raisesValueErrorimmediately at construction, not on the first call. An explicit argument beats the environment variable.base_urlfalls back toSUPERBOOKS_BASE_URL, thenhttps://api.superbooks.io. Pass the root — the SDK appends/mcpitself. Trailing slashes are tolerated.timeoutis 60 seconds per request.max_retriesis 0. See Retries.http_clienttakes your ownhttpx.Clientfor 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.
from→from_. This affectstransactions.list,invoices.list,tracker.list_entries, and fivereportsmethods, sosb.reports.burn_rate(from_="2026-01-01", to="2026-06-30").global→global_, so thesearch_globaltool issb.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:
breakNote 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)
raiseSuperBooksError
├── 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, .dataAnything 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.