Async Python client for Qonto Business API with full type safety.
- Async-first design built on httpx
- Full Pydantic v2 models with
extra="allow"for forward compatibility - Context manager support for clean resource management
- Sandbox support via
QONTO_BASE_URLenvironment variable orbase_url=kwarg - ~40 API methods across 15+ resource types (organizations, memberships, labels, bank accounts, transactions, attachments, clients, invoices, quotes, credit notes, supplier invoices, products, SEPA transfers, bulk transfers, internal transfers, beneficiaries)
- Multipart attachment upload helper for receipts and documents
- Typed exceptions —
AuthenticationError,NotFoundError,ValidationError,RateLimitError,APIError - Generic
PaginatedResponse[T]with Qonto meta-envelope flattening - Rate-limit header tracking via
client.rate_limit_status - py.typed marker for downstream type checking
pip install qodev-qonto-apiOr with uv:
uv add qodev-qonto-apiimport asyncio
from qodev_qonto_api import QontoClient
async def main():
async with QontoClient() as client: # reads QONTO_LOGIN + QONTO_SECRET_KEY
org = await client.get_organization()
print(org.legal_name)
accounts = await client.list_bank_accounts()
for account in accounts:
txns = await client.list_transactions(
bank_account_id=account.id, per_page=10,
)
print(f"{account.iban}: {len(txns.items)} recent")
asyncio.run(main())Set the two required environment variables:
export QONTO_LOGIN="your-org-login"
export QONTO_SECRET_KEY="your-secret-key"Or pass directly:
async with QontoClient(login="your-org-login", secret_key="your-secret-key") as client:
...Important: The Qonto
Authorizationheader is the literal string{login}:{secret}with a real colon. This is not HTTP Basic Authentication — there is no Base64 encoding.
Switch to the staging sandbox via env var:
export QONTO_BASE_URL="https://thirdparty-sandbox.staging.qonto.co/v2"Or via kwarg:
async with QontoClient(base_url="https://thirdparty-sandbox.staging.qonto.co/v2") as client:
...Customize request timeout (default 30 seconds):
async with QontoClient(timeout=60.0) as client:
...Qonto enforces these limits (per IP):
- 1,000 requests / 10 seconds
- 10,000 requests / 10 minutes
Rate-limit headers are captured defensively (Qonto does not officially document header names — the client captures any ratelimit-* or x-ratelimit-* header) and exposed as a raw dict:
async with QontoClient() as client:
await client.list_bank_accounts()
print(client.rate_limit_status) # {"x-ratelimit-remaining": "998", ...}org = await client.get_organization()members = await client.list_memberships()
labels = await client.list_labels()accounts = await client.list_bank_accounts()GET /transactions requires either bank_account_id or iban:
txns = await client.list_transactions(bank_account_id="acc_123", per_page=50)
txn = await client.get_transaction("txn_456")attachment = await client.upload_attachment(
file_path="invoice.pdf",
idempotency_key="unique-key-123",
)
await client.attach_to_transaction(transaction_id="txn_456", attachment_ids=[attachment.id])clients = await client.list_clients()
client_obj = await client.create_client(type="company", name="Acme Corp", email="billing@acme.com")invoices = await client.list_client_invoices()
invoice = await client.create_client_invoice(client_id="cl_1", items=[...])quotes = await client.list_quotes()
credit_notes = await client.list_credit_notes()supplier_invoices = await client.list_supplier_invoices()
si = await client.get_supplier_invoice("si_123")products = await client.list_products()# VoP (Verification of Payee) must be called manually — pass the resulting
# vop_proof_token via extra_fields. See roadmap.
transfer = await client.create_sepa_transfer(
debit_account_id="acc_1",
beneficiary_id="ben_1",
amount="100.00",
currency="EUR",
reference="Invoice INV-001",
vop_proof_token="token-from-verify-payee",
)bulk = await client.create_bulk_transfer(debit_account_id="acc_1", transfers=[...])
internal = await client.create_internal_transfer(
debit_account_id="acc_1",
credit_account_id="acc_2",
amount="50.00",
)beneficiaries = await client.list_beneficiaries()from qodev_qonto_api import (
QontoClient,
QontoError,
AuthenticationError,
NotFoundError,
ValidationError,
RateLimitError,
APIError,
)
try:
async with QontoClient() as client:
await client.get_transaction("does-not-exist")
except AuthenticationError:
print("Invalid login / secret key")
except NotFoundError:
print("Resource not found")
except ValidationError as e:
for err in e.errors:
print(f"{err.get('source', {}).get('pointer')}: {err.get('detail')}")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
except APIError as e:
print(f"API error: {e} (status: {e.status_code})")
except QontoError as e:
print(f"Qonto client error: {e}")make install # Install dependencies
make check # Lint, format, typecheck, typos
make test # Run tests with coverage- OAuth2 / Qonto Connect — partner app integrations
- PSD2 QSeal certificates — regulatory signing
- Cards API — virtual/physical card management
- Webhooks — event subscriptions
- Onboarding API — account creation flows
- VoP integration — Verification of Payee for SEPA transfers (currently manual via
extra_fields) - Payment links
- Embed iframe features
MIT