Skip to content

oskarmodig/spiris

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spiris

PyPI Python CI License: MIT

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.


Table of contents


Install

# One-shot with uv (recommended)
uv tool install spiris

# Or with pipx
pipx install spiris

# Or with pip
pip install spiris

Requires Python 3.10+. Tested on 3.10, 3.11, 3.12 and 3.13.

Configure

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.

  1. Copy .env.example to .env and fill in your credentials:

    cp .env.example .env
    $EDITOR .env
  2. Run the OAuth2 authorization-code flow once — this opens a browser, captures the authorization code on https://localhost:44300/callback, and saves tokens to tokens.json:

    spiris auth

    Spiris requires HTTPS for the redirect URI. spiris generates a self-signed certificate under ./certs/ on first run. Your browser will warn about it — expand AdvancedProceed 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.

Usage

# 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.json

See spiris --help and spiris <command> --help for the full surface.

Commands

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

Library usage

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 raw request
  • ships a paginate(path) iterator that handles oData V4 $page / $pagesize under the hood
  • never raises for non-2xx responses — you get the httpx.Response and decide what to do with it

Scope and status

What works and is covered by tests:

  • OAuth2 authorization_code + refresh_token flow
  • Automatic token refresh on expired access tokens
  • All GET endpoints (companysettings, accounts, vatcodes, fiscalyears, vouchers, suppliers, customers, articles, vatreports, reference data)
  • POST /v2/vouchers including reverse-charge VAT rows
  • POST /v2/attachments + POST /v2/attachmentlinks with base64 upload
  • POST /v2/suppliers (domestic + foreign payment accounts)
  • Pagination via oData V4 $page / $pagesize (max 1000 per page)
  • $filter clauses on list endpoints

Not implemented, rough edges:

  • supplierinvoices / customerinvoices POST — the row schema differs from vouchers and hasn't been exhaustively mapped yet. The generic --body @file flow works; a friendly row syntax doesn't.
  • orders, webshoporders, messagethreads, allocationperiods, partnerresourcelinks — exposed via CRUD commands but not heavily exercised. Use the raw escape hatch for anything unusual.
  • No high-level wrappers for the vatreports flow (filing periods, reconciliation). Read-only access only.

PRs that fill in any of the above are welcome.

Gotchas

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 returns FiscalYearDoesNotExistException. spiris voucher show works 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 @file with a full JSON body for now.
  • VAT codes may be auto-replaced. Spiris validates your VatCodeId against the account and silently swaps in the "correct" one.
  • Chart of accounts is not BAS 2024 verbatim. Always look up spiris accounts list in your own company before assuming a number.
  • DocumentType for attachment links is an enum:
    • 1 = SupplierInvoice
    • 2 = Receipt (not exposed in current API)
    • 3 = Voucher
  • Endpoint naming quirk: /v2/termsofpayments — note the trailing s.
  • Page size caps at 1000. spiris list handles this transparently.

Development

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 pytest

To 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.py

Smoke tests only make read-only requests — they never mutate data.

Contributing

Contributions are welcome. A good PR:

  • has a focused scope (one change per PR)
  • passes ruff check, mypy, and pytest locally
  • 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.

Security

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.

Related projects

References

Disclaimer

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.

License

Released under the MIT License. Copyright © 2026 Oskar Modig and contributors.

About

Unofficial Python CLI and client library for the Spiris (Visma eAccounting / eEkonomi) API

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages