StabisoDocs

REST API

Getting started with the Stabiso API

This page takes you from nothing to your first successful calls: where the API lives, how to get a key, what a key may do, and the first requests. The requests here run against the demo organization in CI, like every example under workflows.

1. Find the API

The ERP is one HTTP service. Set its address once and use it in every example:

export ERP_URL="https://app.stabiso.com"   # the hosted ERP, or your own deployment; no trailing slash

The hosted ERP is at https://app.stabiso.com. Locally it is http://127.0.0.1:8090 (uv run erp serve). Every deployment serves the same paths: /v1/... for the API, /health for a liveness check, /openapi.json for the OpenAPI document, /docs for this documentation (with an interactive explorer of the API at /docs/explorer), /mcp for the MCP endpoint and /app for the web UI. The path prefix /v1 is the version; see versioning.

The health check needs no key:

curl "$ERP_URL/health"
{"status": "ok"}

2. Get an API key

Every other request carries an API key. A key belongs to one organization (the tenant), has a type and a list of scopes, and is shown once when it is created; only its hash is stored. A key looks like erp_<prefix>_<secret>.

  • The first key of an organization is created by an operator, outside the API: uv run erp create-org "Muster AG" prints the organization id and an admin key once. In the hosted deployment ./infra/erp-create-org.sh "Muster AG" stores the key in Secret Manager instead of printing it. uv run erp seed-demo creates the demo organization (Demo AG, CH01, and Demo GmbH, DE01) and prints its admin key; the examples in this documentation start from it.
  • Further keys are created with the API by a key that has the admin scope (POST /v1/api-keys), for example one key per integration or one for each AI agent.

In the examples $ERP_KEY is the organization's first admin key.

export ERP_KEY="erp_...."

Key types and scopes

Actor type For Notes
human A person or an integration acting for one May hold any scope. Humans approve, close periods, finalise VAT returns and release payments, as far as the scopes and the approval policy allow.
agent An AI agent or any automation that must be supervised Never holds approve, admin or audit. The approval policy lets an agent only draft and post, and routes its manual entries and large postings to a person.
system The server's own processes (the CLI, the organization bootstrap, the demo seed) Internal only: POST /v1/api-keys refuses it.
Scope Allows
read Every GET: lists, documents, reports, the ledger. Also the calculations that store nothing (POST /v1/documents/preview, payment proposals, e-invoice checks).
draft Create and change drafts and submit them: journal entries, invoices and bills, parties, payments, payment runs, bank statement imports and matches. What is posted depends on the approval policy.
post Post directly where the policy allows it, reverse entries, release payment files, ignore bank lines.
approve Decide approval requests, close and reopen periods, amend VAT returns.
audit The archive export, a ZIP of every stored file of an entity in a date range with a manifest (GET /v1/archive/export). The Auditor role is read and audit. Never for an agent.
admin Everything, and settings: keys, users, entities, accounts, bank accounts, the approval policy, VAT settings. Implies every other scope.

The scope of every operation is listed in the reference. A key without the scope gets 403. A scope is necessary but not always sufficient: the approval policy can still turn a request into a request for a second person, and some actions are for humans only.

People and their keys

The first key of an organization (the one erp create-org prints) was issued by the system and belongs to no person. It sets the organization up, invites users, drafts and posts, but it can never approve a request, activate a vendor, release a payment file or finalise a VAT return: the four-eyes and segregation rules compare people, and an agent or a key counts as the person behind it (conventions). A key created with the first key is a system key too.

A key that counts as a person comes from a user:

  1. an admin invites the user: POST /v1/users with {"email": "alice@example.ch", "role": "admin"} (roles viewer, accountant, controller, admin; or erp invite-user);
  2. the user signs in to the web UI at $ERP_URL/app with that Google account;
  3. the signed-in user creates API keys for themselves: POST /v1/api-keys with the session cookie and the header X-ERP-UI: 1 (an admin user may do so). A key is the person who created it, however many keys they hold. The key says so itself: bound_to (in the answer that creates it, in GET /v1/api-keys and in GET /v1/me) is the person's actor id, user:<id>, or null for a key of no known person. A headless organization gets its first person from the platform: provisioning invites the first administrator and a second person (section 6).

The examples in these pages use $ERP_KEY (the first key) and two people with keys of their own, $ALICE_KEY (every scope) and $BOB_KEY (read, draft, post, approve). CI creates them exactly as above, with development sign-in standing in for Google. An organization with a single person must either invite a second user or allow that person's own work in its approval policy (release_own_payments, finalize_own_vat_return).

3. First calls

Send the key as a bearer token. GET /v1/me says who the key is and what it may do:

curl "$ERP_URL/v1/me" -H "Authorization: Bearer $ERP_KEY"
{
  "type": "human",
  "id": "<any>",
  "name": "demo admin",
  "org_id": "<uuid>",
  "scopes": ["admin", "approve", "audit", "draft", "post", "read"],
  "auth": "api_key"
}

The header X-API-Key: <key> is accepted instead of Authorization. Without a key, every endpoint except /health answers 401 with the error envelope:

curl "$ERP_URL/v1/entities"
{"error": {"code": "unauthorized", "message": "missing or invalid API key or session"}}

The organization, and the legal entities (companies) it keeps books for:

curl "$ERP_URL/v1/organization" -H "Authorization: Bearer $ERP_KEY"
{"id": "<uuid>", "name": "<any>", "approval_policy": "default"}
curl "$ERP_URL/v1/entities" -H "Authorization: Bearer $ERP_KEY"
[
  {"code": "CH01", "name": "Demo AG", "country": "CH", "functional_currency": "CHF"},
  {"code": "DE01", "name": "Demo GmbH", "country": "DE", "functional_currency": "EUR"},
  {"code": "US01", "name": "Demo Inc.", "country": "US", "functional_currency": "USD"}
]

A report: the trial balance of Demo AG at the end of September. Amounts are decimal strings.

curl "$ERP_URL/v1/entities/CH01/reports/trial-balance?as_of=2026-09-30" \
  -H "Authorization: Bearer $ERP_KEY"
{"entity": "CH01", "currency": "CHF", "as_of": "2026-09-30", "balanced": true}

4. Create a key for an agent or an integration

An admin key creates keys. The answer contains the secret once.

curl -X POST "$ERP_URL/v1/api-keys" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "reporting", "actor_type": "human", "scopes": ["read"]}'
{"id": "<uuid>", "key": "<any>", "actor_type": "human", "scopes": ["read"]}

The new key reads, but it cannot write. A write without the scope is refused with 403:

curl -X POST "$ERP_URL/v1/journal-entries" \
  -H "Authorization: Bearer $READ_KEY" \
  -H "Idempotency-Key: gs-1" \
  -H "Content-Type: application/json" \
  -d '{"entity": "CH01", "entry_date": "2026-09-15", "description": "Rent",
       "lines": [{"account": "6000", "debit": "2500.00"}, {"account": "1000", "credit": "2500.00"}]}'
{"error": {"code": "missing_right", "message": "this API key lacks the 'draft' scope"}}

Keys are listed and revoked by an admin (GET /v1/api-keys, DELETE /v1/api-keys/{id}); the list never shows a secret. An agent key may not hold approve or admin:

curl -X POST "$ERP_URL/v1/api-keys" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "agent with too much", "actor_type": "agent", "scopes": ["read", "approve"]}'
{"error": {"code": "agent_key_scopes"}}

5. Set up a new entity

An entity is a company with its own chart of accounts, fiscal years and books. Creating one needs admin and an Idempotency-Key (see conventions). It opens the fiscal years you name, with the periods of a monthly calendar unless you pass another fiscal_calendar.

curl -X POST "$ERP_URL/v1/entities" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Idempotency-Key: gs-entity-1" \
  -H "Content-Type: application/json" \
  -d '{"code": "CH02", "name": "Muster AG", "country": "CH", "functional_currency": "CHF",
       "chart": "ch_kmu", "fiscal_years": [2026]}'
{"code": "CH02", "name": "Muster AG", "country": "CH", "functional_currency": "CHF"}
curl "$ERP_URL/v1/entities/CH02/periods?on=2026-09-30" -H "Authorization: Bearer $ERP_KEY"
[{"fiscal_year": 2026, "number": 9, "kind": "regular", "status": "open", "start_date": "2026-09-01", "end_date": "2026-09-30"}]

GET /v1/charts lists the chart templates (ch_kmu, the Swiss SME chart; de_hgb, the German chart on the HGB statements; ifrs_generic). An entity created without chart starts from ifrs_generic; GET /v1/charts?country=DE marks the country's standard chart (standard), to send as chart.

6. Operators: provision an organization

This section is for the platform operator, who runs the deployment; a customer never calls it. An organization is created with a platform key, a credential of its own (erpp_<prefix>_<secret>) that works only on /v1/platform and in no organization: a tenant's key or session gets 401 there, and a platform key gets 401 everywhere else. The operator issues it with the command line (erp platform-key create "<name>", shown once or stored in Secret Manager; it expires after 90 days). Its scopes are platform:read (list, status) and platform:provision (create, suspend, reactivate). The examples below are not run by CI (they need a platform key and create an organization); tests/erp/test_platform.py covers them.

export PLATFORM_KEY="erpp_...."
curl -X POST "$ERP_URL/v1/platform/organizations" \
  -H "Authorization: Bearer $PLATFORM_KEY" -H "Idempotency-Key: onboard-muster-1" \
  -H "Content-Type: application/json" \
  -d '{"name": "Muster Treuhand AG",
       "admin": {"email": "petra.muster@example.com", "name": "Petra Muster"},
       "users": [{"email": "hans.keller@example.com", "role": "controller"}],
       "entities": [{"template": "ch_sme", "code": "CH01", "name": "Muster Treuhand AG",
                     "vat_number": "CHE-123.456.788 MWST", "fiscal_years": [2026]}]}'

The answer (201) has the organization, its entities (chart, role accounts, VAT method) and the people invited. Nothing is e-mailed and no key is created: the administrator signs in with Google at $ERP_URL/app, creates their own API key, and starts the onboarding workflow: imports of customers, vendors, accounts, opening balances and open items. A retry with the same Idempotency-Key returns the same answer. GET /v1/platform/organizations lists organizations with their status; POST /v1/platform/organizations/{org_id}/suspend (and /reactivate) makes one read-only and lifts it.

Where next

  • Conventions: idempotency keys, ETag and If-Match, errors, decimals and dates, approvals and four eyes, pagination, tenancy.
  • Workflows: the end-to-end walkthroughs, with exact requests and responses.
  • Reference: every operation, parameter, schema and example.
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d