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-democreates 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
adminscope (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:
- an admin invites the user:
POST /v1/userswith{"email": "alice@example.ch", "role": "admin"}(rolesviewer,accountant,controller,admin; orerp invite-user); - the user signs in to the web UI at
$ERP_URL/appwith that Google account; - the signed-in user creates API keys for themselves:
POST /v1/api-keyswith the session cookie and the headerX-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, inGET /v1/api-keysand inGET /v1/me) is the person's actor id,user:<id>, ornullfor 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,
ETagandIf-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.