High-level Python client for the mBoek bookkeeping API
Project description
mBoek Python client
A high-level, synchronous Python client library for the mBoek bookkeeping API.
Installation
pip install mboek
Requires Python ≥ 3.10 and requests.
Quick start
from mboek import MboekClient
with MboekClient("http://localhost:3000", "admin", "geheim") as client:
# List all company administrations
admins = client.administraties.list()
admin = admins[0]
print(f"Administration: {admin.naam} (id={admin.id})")
# Scope all further calls to one administration
a = client.administratie(admin.id)
# or by name: a = client.administratie(name=admin.naam)
# List fiscal years
years = a.boekjaren.list()
boekjaar = next(y for y in years if y.status.value == "open")
print(f"Open boekjaar: {boekjaar.naam} ({boekjaar.start_datum} → {boekjaar.eind_datum})")
# Get a single fiscal year (one GET request)
bj = a.boekjaar(boekjaar.id)
# or by name: bj = a.boekjaar(name=boekjaar.naam)
# List journals
dagboeken = a.dagboeken.list()
bank = next(d for d in dagboeken if d.dagboek_type.value == "bank")
# List journal entries for the bank dagboek in this boekjaar
entries = bj.dagboek(bank.id).boekingen.list()
for entry in entries[:5]:
print(f" {entry.boeking.datum} {entry.boeking.omschrijving}")
# Generate a balance sheet
balans = bj.reports.balans()
print(f"Activa: {balans.totaal_activa} Passiva: {balans.totaal_passiva} In balans: {balans.in_balans}")
API hierarchy
Resources are accessed through a scoped hierarchy that mirrors the domain model:
MboekClient
├── administraties ← cross-administration resources
├── boekingen ← get / update / delete by ID
├── export_import ← import_administratie
├── maintenance ← database vacuum
└── administratie(id|name=) → AdministratieScope
├── boekjaren ← fiscal years (CRUD + open/close)
├── dagboeken ← journals (CRUD + werkstatus)
├── grootboekrekeningen ← chart of accounts (CRUD + balances + ledger)
├── btw_codes ← VAT codes (CRUD)
├── auto_booking_rules ← automatic booking rules (CRUD)
├── import_ ← bank statement upload
├── export_import ← full export / boekjaar export / import
├── dagboek(id|name=|code=) → Dagboek (rich domain object, no boekjaar scope)
│ ├── naam, code, dagboek_type, … ← always available
│ ├── rerun_regels()
│ ├── suggest(boeking_id)
│ ├── import_boekingen(boekingen)
│ └── with_boekjaar(id=|name=) → Dagboek (boekjaar-scoped)
│ └── boekingen ← list / create
└── boekjaar(id|name=) → Boekjaar (rich domain object)
├── naam, start_datum, eind_datum, status, … ← always available
├── reports ← balance sheet and P&L
├── btw_aangifte ← quarterly VAT returns
├── grootboekrekeningen() ← all accounts with balance for this year
├── grootboekrekening(code=) ← single account with balance, by code
└── dagboek(id|name=|code=) → Dagboek (boekjaar-scoped)
└── boekingen ← list / create
Unified domain objects
Dagboek, Grootboekrekening, and Boekjaar are rich domain objects: they
always carry all data attributes and optionally hold a boekjaar scope that
unlocks additional operations. Every code path returns the same type:
# Both paths return a Dagboek with .naam, .code, .dagboek_type, etc.
dagboek = client.administratie(name="Demo BV").dagboeken.list(code="BTW")[0]
dagboek = client.administratie(name="Demo BV").boekjaar(name="2026").dagboek(code="BTW")
# Both expose .naam (previously the second path had no .naam!)
print(dagboek.naam)
Adding / removing boekjaar scope
# Obtained without scope — data attributes work, boekingen raises ScopeError
dagboek = admin.dagboeken.list(code="BANK")[0]
print(dagboek.naam) # ✓ always works
dagboek.boekingen.list() # ✗ raises ScopeError
# Add scope — returns a new object (original is not mutated)
scoped = dagboek.with_boekjaar(id=10)
scoped.boekingen.list() # ✓ works
# Or look up the boekjaar by name
scoped = dagboek.with_boekjaar(name="2026")
# Remove scope
unscoped = scoped.without_boekjaar()
The same pattern applies to Grootboekrekening.saldo:
gbr = admin.grootboekrekeningen.list(code="1220")[0]
gbr.saldo # ✗ raises ScopeError — no boekjaar
gbr.with_boekjaar(id=10).saldo # ✓ lazily fetched and cached
ScopeError
ScopeError (a subclass of ValueError) is raised when a scope-dependent
method is called without the required scope context. Import it from mboek:
from mboek import ScopeError
Environment variables
| Variable | Description | Default |
|---|---|---|
MBOEK_URL |
Backend base URL | http://localhost:3000 |
MBOEK_USERNAME |
Username for auto-login | (none) |
MBOEK_PASSWORD |
Password for auto-login | (none) |
# No arguments needed when env vars are set
with MboekClient() as client:
admins = client.administraties.list()
Explicitly-passed constructor arguments always override env vars.
Authentication
# Option 1: pass credentials to the constructor (recommended)
client = MboekClient("http://localhost:3000", "admin", "geheim")
# Option 2: use environment variables (MBOEK_URL, MBOEK_USERNAME, MBOEK_PASSWORD)
client = MboekClient()
# Option 3: call login() manually
client = MboekClient("http://localhost:3000")
client.login("admin", "geheim")
# Always call logout() when done (or use the context manager)
client.logout()
Filtering list results
List-based resources support exact-match filters and always return lists.
Common filters are id=, name=, and code= when those fields exist.
Scoped boekingen.list() also supports item= (for stuknummer) and
description= (for omschrijving).
a = client.administratie(1)
# Filter fiscal years by name
boekjaar = a.boekjaren.list(name="2024")[0]
# Filter journals by name or short code (code comparison is case-insensitive)
bank = a.dagboeken.list(name="Bankboek")[0]
bank = a.dagboeken.list(code="bank")[0] # matches "BANK"
# Filter a general-ledger account by name or account code
rekening = a.grootboekrekeningen.list(name="Bank")[0]
rekening = a.grootboekrekeningen.list(code="1220")[0]
# Filter a VAT code by its short code (case-insensitive)
btw = a.btw_codes.list(code="v21")[0] # matches "V21"
# Filter scoped boekingen by boekstuknummer or description
boekingen = a.boekjaar(name="2024").dagboek(code="BANK").boekingen.list(item="INV-42")
boekingen = a.boekjaar(name="2024").dagboek(code="BANK").boekingen.list(description="Factuur 42")
The scope factory methods also accept a name (or code) keyword as a
shorthand. A lookup request is performed and the result must be unique:
NotFoundErroris raised when nothing matchesValueErroris raised when more than one item matches
# Scope by name instead of ID — performs one list lookup per call
admin = client.administratie(name="Demo BV")
bj = admin.boekjaar(name="2024") # one GET /boekjaren/list call
d = bj.dagboek(name="Bankboek") # one GET /dagboeken/list call
d = bj.dagboek(code="BANK") # case-insensitive
# Chained form (each name= or ID triggers at most one HTTP call)
entries = (
client.administratie(name="Demo BV")
.boekjaar(name="2024")
.dagboek(code="BANK")
.boekingen.list()
)
Note:
admin.boekjaar(10)andadmin.dagboek(20)always make one GET request (even when passing a numeric ID) so that the returned object is fully-populated with all data attributes.
Grootboekrekeningen per boekjaar
Use Boekjaar.grootboekrekeningen() to list all accounts enriched with the
transaction count and net balance for a specific fiscal year.
Each item exposes .code, .naam, .transacties, and .saldo as flat attributes.
bj = client.administratie(name="Demo BV").boekjaar(name="2026")
# Iterate all accounts with their year-to-date balance
for rekening in bj.grootboekrekeningen():
print(rekening.code, rekening.naam, rekening.transacties, rekening.saldo)
# Look up a single account by code — raises NotFoundError when not found
rekening = bj.grootboekrekening(code="4000")
print("Saldo 4000:", rekening.saldo)
Creating a journal entry
All boekingsregels must balance (sum(bedrag) == 0).
Amounts are in euros — the library converts to/from cents automatically.
from decimal import Decimal
from datetime import date
from mboek import NewBoekingsregel, Regeltype
regels = [
NewBoekingsregel(
grootboekrekening_id=bank_account_id,
omschrijving="Bank outflow",
bedrag=Decimal("-121.00"), # credit the bank account
),
NewBoekingsregel(
grootboekrekening_id=kosten_id,
omschrijving="Hosting",
bedrag=Decimal("100.00"), # debit costs (netto)
btw_code_id=btw_i21_id,
regeltype=Regeltype.NETTO,
),
NewBoekingsregel(
grootboekrekening_id=btw_vorderen_id,
omschrijving="BTW",
bedrag=Decimal("21.00"), # debit VAT receivable
regeltype=Regeltype.BTW,
netto_ref=1, # index of the netto regel above
),
]
bj_dagboek = client.administratie(admin_id).boekjaar(boekjaar_id).dagboek(bank_dagboek_id)
entry = bj_dagboek.boekingen.create(
regels=regels,
datum=date(2024, 3, 15),
omschrijving="Hosting invoice March",
# boekjaar_id is injected automatically from the scope
)
print(f"Created boeking {entry.boeking.id}")
Alternatively, you can reference accounts by name or code instead of a numeric ID — the library resolves them automatically (with caching):
regels = [
NewBoekingsregel(grootboekrekening_code="1220", omschrijving="Bank", bedrag=Decimal("-100.00")),
NewBoekingsregel(grootboekrekening_naam="Kosten internet", omschrijving="Hosting", bedrag=Decimal("100.00")),
]
Updating and deleting a journal entry
Every Boeking returned from the API carries a client reference, so you can
call update() and delete() directly on the object — no need to go through
client.boekingen again.
# Retrieve a single entry by ID
boeking = client.boekingen.get(100)
# Update header fields (pass only the fields you want to change)
updated = boeking.update(
omschrijving="Corrected description",
gecontroleerd=True,
)
print(updated.omschrijving) # "Corrected description"
# Entries returned from list() are also scoped
entries = (
client.administratie(admin_id)
.boekjaar(boekjaar_id)
.dagboek(dagboek_id)
.boekingen.list()
)
for entry in entries:
if entry.omschrijving == "WRONG":
entry.delete()
update() accepts the same keyword arguments as BoekingenResource.update()
(all optional): datum, omschrijving, stuknummer, status,
tegenpartij_naam, tegenpartij_iban, gecontroleerd, auto_geboekt, and
regels (full replacement set of boekingsregels). It returns a fresh
Boeking with the updated data.
delete() permanently removes the boeking and all its boekingsregels.
Both methods raise ScopeError when called on a Boeking that was not
obtained via a live client (e.g. built manually or deserialised from a backup).
Setting up a new administration
from datetime import date
# 1. Create the administration
admin = client.administraties.create(naam="My Company BV", btw_nummer="NL123456789B01")
a = client.administratie(admin.id)
# 2. Seed the standard Dutch chart of accounts
a.grootboekrekeningen.seed_rgs()
# 3. Seed the standard Dutch BTW (VAT) codes
a.btw_codes.seed_defaults()
# 4. Create a fiscal year
boekjaar = a.boekjaren.create(
naam="2024",
start_datum=date(2024, 1, 1),
eind_datum=date(2024, 12, 31),
)
# 5. Set it as the current/active year
a.boekjaren.set_huidig(boekjaar.id)
BTW-aangifte (VAT return) workflow
bj = client.administratie(admin_id).boekjaar(boekjaar_id)
# 1. Calculate the Q1 VAT return (creates a concept)
aangifte = bj.btw_aangifte.berekenen(kwartaal=1)
print(f"Q1 VAT: {aangifte.r5g}") # positive = te betalen, negative = te ontvangen
# 2. Close the fiscal year (required before vastleggen)
client.administratie(admin_id).boekjaren.afsluiten(boekjaar_id)
# 3. Lock the aangifte and create the balancing boeking
definitief = bj.btw_aangifte.vastleggen(aangifte.id)
Bank statement import
from pathlib import Path
result = client.administratie(admin_id).import_.upload(Path("afschrift-jan.940"))
print(f"Imported {result.imported} transactions, skipped {result.skipped} duplicates")
Export and import
import json
a = client.administratie(admin_id)
# Full export to a JSON file
payload = a.export_import.export_administratie()
with open("backup.json", "w") as f:
json.dump(payload, f, indent=2)
# Export a single boekjaar
bj_payload = a.export_import.export_boekjaar(boekjaar_id)
# Restore a full backup into a new administration
with open("backup.json") as f:
payload = json.load(f)
client.export_import.import_administratie(payload)
Automatic booking rules
from mboek.models._enums import ActieType
a = client.administratie(admin_id)
rule = a.auto_booking_rules.create(
naam="Hosting Duitsland",
actie_type=ActieType.ENKEL,
tegenpartij_iban_patroon="DE75512308000000060004",
lines=[...],
)
# Re-apply all rules to unprocessed entries in a dagboek (year-agnostic)
updated = a.dagboek(bank_dagboek_id).rerun_regels()
print(f"Auto-booked {len(updated)} entries")
Reports
bj = client.administratie(admin_id).boekjaar(boekjaar_id)
# Balance sheet
balans = bj.reports.balans()
print(f"Activa: {balans.totaal_activa} Passiva: {balans.totaal_passiva}")
print(f"In balans: {balans.in_balans}")
# Profit & loss
wv = bj.reports.winst_verlies()
print(f"Netto resultaat: {wv.netto_resultaat}")
Error handling
from mboek import (
AuthError, # 401 Unauthorized
ForbiddenError, # 403 Forbidden
NotFoundError, # 404 Not Found
ConflictError, # 409 Conflict
ValidationError, # 422 Unprocessable Entity
RateLimitError, # 429 Too Many Requests
ScopeError, # scope-dependent method called without required scope
MboekError, # base for all API errors
)
try:
client.administratie(admin_id).boekjaren.afsluiten(boekjaar_id)
except ConflictError as e:
print(f"Cannot close: {e}") # e.g. already closed
except NotFoundError:
print("Boekjaar not found")
All HTTP exceptions expose:
e.status_code— HTTP status codee.detail— parsed response body (dict or str)
ScopeError (a subclass of ValueError) is raised when a scope-dependent
method is called without the required scope context and has no status_code.
Dutch accounting glossary
| Dutch term | English equivalent |
|---|---|
| Administratie | Company administration / set of books |
| Boekjaar | Fiscal / financial year |
| Dagboek | Journal / sub-ledger |
| Grootboekrekening | General-ledger account |
| Boeking | Journal entry |
| Boekingsregel | Journal entry line |
| BTW | VAT (value-added tax) |
| BTW-aangifte | VAT return (quarterly) |
| Netto bedrag | Net amount (excluding VAT) |
| Te betalen BTW | Output VAT (VAT payable to the tax office) |
| Te vorderen BTW | Input VAT (VAT reclaimable) |
| Balans | Balance sheet |
| Winst & verlies | Profit & loss statement |
| Activa | Assets |
| Passiva | Liabilities + equity |
| Kosten | Costs / expenses |
| Opbrengsten | Revenues |
| Debet | Debit |
| Credit | Credit |
| Saldo | Balance |
| Stuknummer | Document / invoice reference number |
| Tegenpartij | Counterparty |
| Banktransacties | Bank transactions |
| Automatische boekregels | Automatic booking rules |
Development
git clone https://github.com/newinnovations/mboek-python-client.git
cd mboek-python-client
uv venv
uv pip install -e ".[dev]"
uv run pytest
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mboek-0.3.0.tar.gz.
File metadata
- Download URL: mboek-0.3.0.tar.gz
- Upload date:
- Size: 97.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7963059f035efef0b35a56036d30e3113fcf0c48836fe506b6691877de5a2be8
|
|
| MD5 |
10a8fc8268bba81ade2eb69ddefb336c
|
|
| BLAKE2b-256 |
9902686ca2509b6326e80fa01c864c735c3b2ba55f23bf7375bfd03a2e6b417b
|
Provenance
The following attestation bundles were made for mboek-0.3.0.tar.gz:
Publisher:
publish.yml on newinnovations/mboek-python-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mboek-0.3.0.tar.gz -
Subject digest:
7963059f035efef0b35a56036d30e3113fcf0c48836fe506b6691877de5a2be8 - Sigstore transparency entry: 1413063126
- Sigstore integration time:
-
Permalink:
newinnovations/mboek-python-client@8b53d27695c56d58da531493e0980a310a93a207 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/newinnovations
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8b53d27695c56d58da531493e0980a310a93a207 -
Trigger Event:
release
-
Statement type:
File details
Details for the file mboek-0.3.0-py3-none-any.whl.
File metadata
- Download URL: mboek-0.3.0-py3-none-any.whl
- Upload date:
- Size: 70.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
21602e2b36aad95e40a7a790e282d44ea68467f956e370041ae2ee21c238ecd9
|
|
| MD5 |
1c35536b29cb8ed48d0b600c4b2b80f5
|
|
| BLAKE2b-256 |
6f58fb7cab5a000c1b99f1f9a3ce19f296713d20346259f7547095c06b457550
|
Provenance
The following attestation bundles were made for mboek-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on newinnovations/mboek-python-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mboek-0.3.0-py3-none-any.whl -
Subject digest:
21602e2b36aad95e40a7a790e282d44ea68467f956e370041ae2ee21c238ecd9 - Sigstore transparency entry: 1413063222
- Sigstore integration time:
-
Permalink:
newinnovations/mboek-python-client@8b53d27695c56d58da531493e0980a310a93a207 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/newinnovations
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8b53d27695c56d58da531493e0980a310a93a207 -
Trigger Event:
release
-
Statement type: