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
- 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 insrc/erp/api/params.py). - 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'smodel_config = ConfigDict(json_schema_extra={"examples": [...]})). Use the demo organization's values. - Run
uv run python -m tools.apidocsand commit the result. - 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_URLis the server and$ERP_KEYthe first admin key of the demo organization (it belongs to no person);$ALICE_KEYand$BOB_KEYare keys of two people, for approving, releasing payments and finalising VAT (getting started).capture=NAME:pathkeeps a value of the JSON answer as$NAMEfor later requests of the page.- A request without a
responseblock 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) usescontains: 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-independentin 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