Important
Personal-use, unofficial package. aiomonzo is an independent community
project. It is not developed, maintained, supported, approved, or endorsed by
Monzo Bank.
Monzo states that its Developer API is not suitable for public applications and may be used only with your own account or a small set of users you explicitly allow. This package is therefore intended for personal projects and small private integrations, not public customer-facing banking applications. See the Monzo Developer API introduction.
aiomonzo is an unofficial, fully asynchronous and typed Python client for the
Monzo Developer API. It provides API, OAuth, retry,
validation, and resource-lifecycle primitives while leaving credential storage,
tenancy, user interfaces, and deployment policy to the application using it.
The package supports Python 3.12, 3.13, and 3.14.
- Fully asynchronous API and OAuth operations using
httpx. - Strict, frozen Pydantic models that preserve new provider fields.
- Static tokens, caller-owned OAuth token storage, and custom token providers.
- Automatic refresh with rotated refresh-token replacement.
- Optional distributed refresh locking for multi-process deployments.
- Bounded timeouts and retries that respect mutation idempotency.
- Typed, secret-safe exceptions that do not retain provider response bodies.
- Explicit async resource ownership and a
py.typedmarker.
python -m pip install aiomonzoWith uv:
uv add aiomonzo| Guide | Contents |
|---|---|
| Getting started | Installation, API Playground tokens, and first requests |
| OAuth setup | Creating a Monzo client and completing authorization |
| Token storage | Durable storage, refresh rotation, and multi-process locking |
| API reference | Constructor options, methods, models, and pagination |
| Errors and retries | Exception hierarchy, retry behavior, and recovery |
| Security | Credential, state, transport, webhook, and logging guidance |
| Troubleshooting | Common Monzo authorization and API failures |
| Development | Local setup, validation, packaging, and release workflow |
The same guides are published in the GitHub Wiki.
A static token is useful for short-lived development and API Playground experiments. Generate one in the Monzo developer tools, approve access in the Monzo mobile app when prompted, and avoid hard-coding or committing it.
import asyncio
import os
from aiomonzo import MonzoClient
async def main() -> None:
async with MonzoClient(
access_token=os.environ["MONZO_ACCESS_TOKEN"],
) as monzo:
identity = await monzo.who_am_i()
accounts = await monzo.list_accounts()
print(identity.user_id, [account.id for account in accounts])
asyncio.run(main())For long-lived access, follow the OAuth setup guide.
Applications using Monzo OAuth supply an OAuthClientConfig and a TokenStore.
The store controls encryption, tenant isolation, persistence, and atomic token
replacement. aiomonzo never creates durable token storage.
from aiomonzo import MonzoClient, OAuthClientConfig, OAuthToken, TokenStore
class ApplicationTokenStore(TokenStore):
async def load(self) -> OAuthToken | None:
... # Load and decrypt for the current application user.
async def save(self, token: OAuthToken) -> None:
... # Atomically replace access and rotated refresh tokens.
async def clear(self) -> None:
... # Remove the current user's token set.
oauth = OAuthClientConfig(
client_id="monzo-client-id",
client_secret=load_monzo_client_secret(),
redirect_uri="https://app.example.com/oauth/monzo/callback",
)
client = MonzoClient(oauth=oauth, token_store=ApplicationTokenStore())
authorization = client.create_authorization_request()
# Store authorization.state in a user-bound, short-lived session before
# redirecting the browser to authorization.url.At the callback, pass the code and both state values to
exchange_authorization_code. State comparison is constant-time. When a token
expires or Monzo rejects it, refresh is serialized within the provider and the
complete replacement token set is passed to TokenStore.save.
Monzo can rotate refresh tokens. A durable save implementation must replace
the complete token set atomically; saving only the access token can make the
next refresh impossible.
By default, aiomonzo serializes refreshes within one process. If multiple
processes or hosts share the same durable token record, pass a
refresh_lock_factory backed by a distributed lock, such as a database
advisory lock or Redis lock. The lock must be scoped to the same user and OAuth
client as the token record and remain held while the token is reloaded,
refreshed, and atomically replaced.
The factory returns an async context manager:
from contextlib import AbstractAsyncContextManager
from aiomonzo import MonzoClient
def refresh_lock_factory() -> AbstractAsyncContextManager[None]:
return application_locks.monzo_oauth(user_id)
client = MonzoClient(
oauth=oauth,
token_store=ApplicationTokenStore(),
refresh_lock_factory=refresh_lock_factory,
)All processes sharing that token record must use the same lock key. A single-process application can omit this option.
For multi-user applications, gateways, or private credential brokers, implement
the narrow AccessTokenProvider protocol:
from aiomonzo import AccessTokenProvider, MonzoClient
class BrokerAccessTokenProvider(AccessTokenProvider):
async def get_access_token(self) -> str:
return await obtain_short_lived_token()
async def refresh_after_rejection(self, rejected_access_token: str) -> str:
return await replace_rejected_token(rejected_access_token)
client = MonzoClient(access_token_provider=BrokerAccessTokenProvider())The provider owns storage and refresh policy. MonzoClient requests a token
only when sending a Monzo API request and never exposes it through model data.
MonzoClient currently provides:
who_am_ilist_accountsget_balancelist_potsdeposit_into_potwithdraw_from_potget_transactionlist_transactionsannotate_transactionregister_webhooklist_webhooksdelete_webhookcreate_authorization_requestexchange_authorization_coderefresh_access_tokenlogout
Amounts use Monzo's integer minor units. Transaction listing returns one bounded
page and accepts since, before, limit, and merchant expansion controls;
the caller owns pagination policy.
By default, MonzoClient owns one pooled httpx.AsyncClient. Close it with an
async context manager or await client.aclose(). If an application injects an
existing httpx.AsyncClient, the application retains ownership and must close
it.
Default connection limits, timeouts, redirects, and retries are bounded. Read-only requests can be retried for transient failures. Mutating requests are not replayed unless the operation has an idempotency contract.
Failures derive from MonzoClientError, with typed subclasses for
configuration, request validation, authentication, permission, rate limiting,
timeouts, transport, provider response decoding, provider response validation,
and token-store failures.
Exceptions deliberately avoid access tokens, OAuth secrets, provider bodies, and credential-bearing URLs. Applications should still avoid logging arbitrary financial model data.
uv sync --dev --frozen
make check
uv run pre-commit run --all-filesmake check runs Ruff, strict mypy, the full test suite, distribution metadata
checks, artifact-content checks, and a clean installation smoke test.
Do not report vulnerabilities with real Monzo credentials or financial data. Use GitHub private vulnerability reporting when available.
Read the security guide before deploying OAuth token storage or using the client in a multi-process service.
- Monzo Developer tools
- Monzo Developer API reference
- Monzo authentication documentation
- Monzo Open Banking documentation
MIT. This project is not affiliated with or endorsed by Monzo Bank.