StabisoDocs

REST API

API documentation

Everything a headless user needs to work with the platform over HTTP, without the web UI:

  • the Stabiso API: a double-entry financial system of record (ledger, receivables and payables, banking, assets, VAT returns, period close) with scoped keys for people and AI agents;
  • the data-vNext API: the reference data and deterministic rules under it (VAT and sales tax, exchange rates, calendars, depreciation, accounting standards).

Both are REST APIs under /v1 that speak JSON, and both also offer the same functions as MCP tools. The ERP is REST first: the web UI, the MCP tools and the built-in agents only do what a REST operation does.

Start here

I want to Read
make my first call Getting started: the address, a key, scopes, first requests
know how every call behaves Conventions: authentication, idempotency keys, ETag and If-Match, errors, decimals and dates, approvals and four eyes, limits, tenancy
see a business process end to end Workflows: exact requests and responses
bring a new customer onto the platform Onboard a new customer: provisioning, people, imports of the existing books, the first invoice
look up an operation ERP reference and data-vNext reference
import the API into a tool or generate a client erp-openapi.json and datavnext-openapi.json
connect an AI assistant MCP and the list of tools

Base URLs

The address depends on the environment. The path structure is the same everywhere.

Environment Stabiso data-vNext
Local http://127.0.0.1:8090 (uv run erp serve) http://127.0.0.1:8080 (uv run datavnext serve)
Hosted https://app.stabiso.com the reference data service of the deployment

The hosted ERP runs in Google Cloud, region europe-west6 (Zürich). Put its address in $ERP_URL (and the reference data service's in $DATAVNEXT_URL), as the examples do. Each service serves /health (no key), /openapi.json (the live OpenAPI document) and /mcp; the ERP also serves this documentation at /docs and an interactive API explorer at /docs/explorer.

Versioning

The path prefix is the API version: /v1. A version changes additively only: new operations, new optional request fields, new response fields and new enum values. Existing paths, fields and meanings stay. A client must ignore what it does not know. A breaking change would be published under /v2 next to /v1; the recorded exceptions (see conventions), and every other change integrators notice, are in the ERP release notes. The OpenAPI info.version is the release of the service, not the API version.

How this documentation is made, and kept true

The reference and the examples are not written by hand, so that they cannot drift from the code.

What Where it comes from How it is kept current
The OpenAPI documents (*-openapi.json) The running apps: uv run erp openapi, uv run datavnext openapi. They add, from the code, the required scope (x-required-scope), whether Idempotency-Key is required (x-idempotency), If-Match and ETag, security schemes and the error envelope. tests/api/test_api_docs.py fails in CI when a committed file differs from what the code produces
The reference pages (erp/, datavnext/, mcp-tools.md) tools/apidocs, a pure function of the OpenAPI documents and the MCP tool lists The same test
Descriptions, parameter and field descriptions, request examples The code: handler docstrings, Query(description=...), Field(description=...) and model_config = ConfigDict(json_schema_extra={"examples": [...]}) on the models in src/erp/contracts tests/erp/test_rest_parity.py and tests/erp/test_api_docs_source.py fail when an operation has no description, a JSON body no example, or a request field or parameter no description
The guides (getting started, conventions, MCP) and the workflows Written by hand The curl examples in getting started and the workflows are executed in CI against the demo organization (below)
The public site at /docs tools/docsite builds it from these pages and the guides (docs/site/nav.toml) when the image is built python -m tools.docsite --check fails on a broken link, an unpublished guide or an undescribed Autopilot outcome or webhook event; --changes fails a change of the API or MCP tools that changes no guide

To regenerate after changing any endpoint, model or description:

uv run python -m tools.apidocs            # writes docs/api/*-openapi.json and the reference pages
uv run python -m tools.apidocs --check    # what CI does: lists what is stale

The OpenAPI document also feeds the web UI's typed client: after changing it run npm run gen:api in ui/ as well (CI checks both). Never edit a generated file. After a rebase that touches routers or models, the generated files conflict or go stale: take either side and regenerate.

Adding or changing an endpoint

  1. Write the handler's docstring: what it does, what it refuses (409, 412, 422) and what the answer means. Describe every parameter (Query(description=...); use the shared path types in src/erp/api/params.py).
  2. Describe every field of the request model in src/erp/contracts/<area>.py (Field(description=...)) and give the JSON request body an example (the model's model_config = ConfigDict(json_schema_extra={"examples": [...]})). Use the demo organization's values.
  3. Run uv run python -m tools.apidocs and commit the result.
  4. If the endpoint is part of a business process people follow, add its steps to a workflow page, or write a new page.

Executable examples

The pages under docs/api/workflows and getting started are tests (the one exception, onboarding a customer, needs a platform key and a new organization; it is listed in NOT_RUN of tests/erp/test_docs_examples.py with the tests that run each step instead). tests/erp/test_docs_examples.py seeds the demo organization (Demo AG CH01, Demo GmbH DE01, the same data as erp seed-demo), then runs every example of each page, in order, through the ERP's TestClient, and compares the answer with what the page shows. A page that no longer matches the API fails CI with the page, the line, the request and the answer.

An example is a fenced block with example in its info string, holding one curl command, followed by the answer in a block with response in its info string:

```bash example capture=ENTRY_ID:entry.id
curl -X POST "$ERP_URL/v1/journal-entries" \
  -H "Authorization: Bearer $ERP_KEY" -H "Idempotency-Key: je-1" \
  -H "Content-Type: application/json" -d '{ ... }'
```

```json response status=201
{"result": {"status": "posted"}, "entry": {"id": "<uuid>"}}
```
  • $ERP_URL is the server and $ERP_KEY the first admin key of the demo organization (it belongs to no person); $ALICE_KEY and $BOB_KEY are keys of two people, for approving, releasing payments and finalising VAT (getting started). capture=NAME:path keeps a value of the JSON answer as $NAME for later requests of the page.
  • A request without a response block must answer 2xx; status=422 (and so on) names another status.
  • The answer is compared as a subset: every key written on the page must be in the answer with the same value; the answer may have more. Lists have the same length unless the last element is "..."; "<uuid>", "<date-time>" and "<any>" match values you cannot predict. A text answer (XML, SVG) uses contains: each line of the block must occur in the body.
  • An example that calls an endpoint whose date defaults to today (the parameter says "default today") must pass the date explicitly, or the test fails, because the answer would depend on the day it runs. clock-independent in the info string exempts a step that asserts nothing derived from today.
  • Other fenced blocks are ignored by the runner: use them for illustrations that are not run.

The full syntax is in the docstring of tests/erp/docs_examples.py.

Other documentation

Everything else, including the plain-language list of features, starts at the documentation home.

  • ERP architecture, ledger invariants, agents and the API style guide (rules for new endpoints, and the behaviour of each feature)
  • data-vNext data model and data sources
  • Engineering standards
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d