Unofficial Python CLI and client library for the Spiris API (formerly Visma eAccounting / Visma eEkonomi) — the SaaS accounting system widely used by Swedish small businesses and sole traders (enskilda firmor).
spiris gives you a first-class command line over the full REST API: create
vouchers, upload receipts, manage suppliers and customers, query VAT reports,
paginate any list endpoint. Everything is JSON-in / JSON-lines-out, so it
composes cleanly with jq, xargs, shell pipelines, and scripts.
🇸🇪 Svenska utvecklare:
spirisär en inofficiell Python-klient för Visma Spiris (f.d. Visma eEkonomi / eAccounting) — både som bibliotek (from spiris import SpirisClient) och som kommandorad (spiris voucher create …). Användbart för att automatisera löpande bokföring, kvittohantering och periodisk avstämning för enskild firma och aktiebolag.
⚠️ Unofficial. Not affiliated with, maintained by, or endorsed by Visma or Spiris. Use at your own risk. See the Disclaimer section below — please read it before using this in production.
- Install
- Configure
- Usage
- Commands
- Library usage
- Scope and status
- Gotchas
- Development
- Contributing
- Security
- Related projects
- References
- Disclaimer
- License
# One-shot with uv (recommended)
uv tool install spiris
# Or with pipx
pipx install spiris
# Or with pip
pip install spirisRequires Python 3.10+. Tested on 3.10, 3.11, 3.12 and 3.13.
You need a Spiris API client (client_id + client_secret). For development,
request sandbox access by e-mailing api@spiris.se. For production, you
need to go through Spiris's onboarding with a real organisation number.
-
Copy
.env.exampleto.envand fill in your credentials:cp .env.example .env $EDITOR .env -
Run the OAuth2 authorization-code flow once — this opens a browser, captures the authorization code on
https://localhost:44300/callback, and saves tokens totokens.json:spiris auth
Spiris requires HTTPS for the redirect URI.
spirisgenerates a self-signed certificate under./certs/on first run. Your browser will warn about it — expand Advanced → Proceed to localhost.
The resulting tokens.json contains your access and refresh tokens. Treat
it like a password — it is written with mode 0600 by default. The client
refreshes expired access tokens automatically on 401 responses.
# Show current company
spiris companysettings
# Chart of accounts (human-readable table)
spiris accounts list --search 4535
# VAT codes (needed for voucher rows with VAT)
spiris vatcodes list
# Create a voucher from the command line
spiris voucher create \
--date 2026-04-09 \
--text "Anthropic API" \
--debit 4535:239.93 \
--credit 1930:239.93
# Reverse-charge VAT (EU/US SaaS): four rows, one with VAT code
spiris voucher create \
--date 2026-04-09 \
--text "Google Workspace" \
--debit 4535:242.02 --credit 1930:242.02 \
--debit 2645:60.50 --credit 2614:60.50 \
--vat 2645:<vatcode-id> --vat 2614:<vatcode-id>
# Upload a PDF receipt and link it to the voucher in one call
spiris attachment upload receipt.pdf --link <voucher-id> --type voucher
# Paginate any list endpoint — JSON lines, pipe to jq
spiris list /v2/suppliers --filter "CountryCode eq 'SE'" | jq .Name
# Escape hatch: call any endpoint directly
spiris raw GET /v2/vatreports
spiris raw POST /v2/notes --body @note.jsonSee spiris --help and spiris <command> --help for the full surface.
| Command | Description |
|---|---|
spiris auth |
Run the OAuth2 authorization-code flow |
spiris companysettings |
Show company info |
spiris accounts list |
Chart of accounts |
spiris vatcodes list |
VAT codes with IDs |
spiris voucher list/show/create |
Journal entries (verifikat) |
spiris attachment upload/link |
Receipts and other attachments |
spiris supplier/customer/article <verb> |
CRUD for master data |
spiris supplierinvoice/customerinvoice <verb> |
Invoices |
spiris vatreport |
VAT reports |
spiris countries/currencies/units/banks/... |
Reference data |
spiris list <path> |
Paginate any list endpoint |
spiris raw <method> <path> |
Raw HTTP escape hatch |
The CLI is a thin wrapper around SpirisClient. You can use the client
directly in Python:
from spiris import SpirisClient
with SpirisClient.from_env() as client:
settings = client.get("/v2/companysettings").json()
print(settings["Name"])
for account in client.paginate("/v2/accounts"):
print(account["Number"], account["Name"])
resp = client.post("/v2/vouchers", {
"VoucherDate": "2026-04-09",
"VoucherText": "Example",
"NumberSeries": "A",
"Rows": [
{"AccountNumber": 4535, "DebitAmount": 100.0, "CreditAmount": 0.0},
{"AccountNumber": 1930, "DebitAmount": 0.0, "CreditAmount": 100.0},
],
})
resp.raise_for_status()SpirisClient is a thin wrapper around httpx.Client. It:
- refreshes the OAuth access token automatically on
401 - exposes
get,post,put,delete, and rawrequest - ships a
paginate(path)iterator that handles oData V4$page/$pagesizeunder the hood - never raises for non-2xx responses — you get the
httpx.Responseand decide what to do with it
What works and is covered by tests:
- OAuth2 authorization_code + refresh_token flow
- Automatic token refresh on expired access tokens
- All
GETendpoints (companysettings, accounts, vatcodes, fiscalyears, vouchers, suppliers, customers, articles, vatreports, reference data) POST /v2/vouchersincluding reverse-charge VAT rowsPOST /v2/attachments+POST /v2/attachmentlinkswith base64 uploadPOST /v2/suppliers(domestic + foreign payment accounts)- Pagination via oData V4
$page/$pagesize(max 1000 per page) $filterclauses on list endpoints
Not implemented, rough edges:
supplierinvoices/customerinvoicesPOST— the row schema differs from vouchers and hasn't been exhaustively mapped yet. The generic--body @fileflow works; a friendly row syntax doesn't.orders,webshoporders,messagethreads,allocationperiods,partnerresourcelinks— exposed via CRUD commands but not heavily exercised. Use therawescape hatch for anything unusual.- No high-level wrappers for the
vatreportsflow (filing periods, reconciliation). Read-only access only.
PRs that fill in any of the above are welcome.
Things I learned the hard way — expect a real-world API with real-world quirks.
- The redirect URI must be HTTPS. Spiris rejects
http://localhost:.... - Vouchers cannot be deleted.
DELETE /v2/vouchers/{id}returns 405, by design — Swedish accounting law (BFL 5:5) requires that posted entries stay posted. To correct a mistake, book a reversing entry. GET /v2/vouchers/{id}is broken. Spiris returnsFiscalYearDoesNotExistException.spiris voucher showworks around this by filtering on the list endpoint instead.- Attachments linked to vouchers can't be deleted. 400 with
CanNotBeDeletedException. Same reason as above. - Supplier invoice row schema differs from voucher rows. Vouchers use
DebitAmount/CreditAmount; supplier invoices use a different shape. Pass--body @filewith a full JSON body for now. - VAT codes may be auto-replaced. Spiris validates your
VatCodeIdagainst the account and silently swaps in the "correct" one. - Chart of accounts is not BAS 2024 verbatim. Always look up
spiris accounts listin your own company before assuming a number. DocumentTypefor attachment links is an enum:1= SupplierInvoice2= Receipt (not exposed in current API)3= Voucher
- Endpoint naming quirk:
/v2/termsofpayments— note the trailings. - Page size caps at 1000.
spiris listhandles this transparently.
git clone https://github.com/oskarmodig/spiris.git
cd spiris
uv venv
uv pip install -e ".[dev]"
# Lint + type check + unit tests
uv run ruff check src tests
uv run mypy src
uv run pytestTo run the live smoke tests, set sandbox credentials in your shell:
export SPIRIS_CLIENT_ID=...
export SPIRIS_CLIENT_SECRET=...
export SPIRIS_TOKENS_PATH=/absolute/path/to/tokens.json
uv run pytest tests/test_smoke.pySmoke tests only make read-only requests — they never mutate data.
Contributions are welcome. A good PR:
- has a focused scope (one change per PR)
- passes
ruff check,mypy, andpytestlocally - includes a test when it changes or adds behavior
- describes why the change is needed, not just what it does
Before starting a large change, open an issue to discuss the approach.
If you find a security issue — credential leakage, OAuth misuse, injection into API calls, cert-handling bugs — please report it privately by opening a GitHub security advisory instead of a public issue. I'll respond as time allows; this is a best-effort maintained project.
pwitab/visma— older Python ORM, unmaintained since 2018.jimmystridh/spiris-rust— Rust client library.espen/visma_eaccounting— Ruby wrapper.simenandre/eaccounting— JavaScript SDK.
- Official Spiris / eAccounting API docs
- Visma Developer Portal
- Bokföringsnämnden (BFN) — Swedish accounting authority
- Skatteverket — enskild firma
- BAS-kontoplan — Swedish standard chart of accounts
Please read this before using spiris in production.
This project is an independent, community-maintained client for a third-party API. It is not an official Visma or Spiris product, not supported by Visma or Spiris, and the author has no commercial, employment, or agency relationship with Visma, Spiris, or any Visma Group company.
No warranty. The software is provided "as is", without warranty of any kind — express or implied — including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. See the LICENSE for the full legal text.
You bear the risk. By using spiris, you accept full responsibility
for any and all consequences, including but not limited to:
- incorrect bookkeeping entries or missing entries
- incorrect VAT calculations, reports, or filings
- data loss, corruption, or unintended modification of accounting data
- financial penalties, fines, or tax assessments arising from errors
- API downtime, rate limiting, or breaking changes on Spiris's side
- bugs, regressions, or unintended behavior in this software
Not accounting advice. Nothing in this repository — code, comments, documentation, or examples — constitutes accounting, legal, or tax advice. Swedish accounting law (Bokföringslagen) places the duty of proper bookkeeping squarely on the business owner. A tool like this does not transfer or reduce that duty. If you are unsure whether a transaction should be booked a particular way, consult an authorised accountant (auktoriserad redovisningskonsult) or the Swedish Tax Agency (Skatteverket) — not an open-source tool on GitHub.
Verify before you post. Especially during initial setup, always confirm in the Spiris web UI that the entries created by this tool match your expectations. Use a sandbox company until you're confident.
No support guarantee. Issues and pull requests are triaged on a best-effort basis. There is no service-level agreement, no guaranteed response time, and no paid support. If you need guaranteed support, you should use an official Visma partner or integrator instead.
Trademarks. "Spiris", "Visma", "eAccounting", "eEkonomi", and related marks are trademarks of Visma Group or their respective owners. This project uses those names for descriptive, nominative-fair-use purposes only — to identify which API the software talks to.
Released under the MIT License. Copyright © 2026 Oskar Modig and contributors.