Platform
For the platform operator only, with a platform key or a session of the operator portal: provision organizations, list and open them, suspend and reactivate them, invite their administrators, mark a demo, export an organization's data for its offboarding, read the audit record. A tenant's keys and sessions do not work here, and a platform credential works nowhere else.
| Operation | Summary | Scope | Idempotency-Key |
|---|---|---|---|
GET /v1/platform/audit |
List the operator's audit record | platform:read |
|
POST /v1/platform/auth/google |
Sign in to the operator portal with Google | ||
POST /v1/platform/auth/logout |
Sign out of the operator portal | platform:read |
|
GET /v1/platform/charts |
List chart templates | platform:read |
|
GET /v1/platform/health |
Service health: SLOs, error budget, latency, uptime and deploy | platform:read |
|
GET /v1/platform/me |
Who the platform credential is | platform:read |
|
GET /v1/platform/models |
Show the models behind the built-in agents | platform:read |
|
PUT /v1/platform/models |
Choose the models behind the built-in agents | platform:provision |
|
POST /v1/platform/models/check |
Check that a model answers | platform:provision |
|
GET /v1/platform/organizations |
List organizations | platform:read |
|
POST /v1/platform/organizations |
Provision an organization | platform:provision |
required |
GET /v1/platform/organizations/{org_id} |
Get an organization | platform:read |
|
POST /v1/platform/organizations/{org_id}/admins |
Invite or show again an organization's first administrator | platform:provision |
|
POST /v1/platform/organizations/{org_id}/delete |
Delete an organization | platform:provision |
|
GET /v1/platform/organizations/{org_id}/exports |
List an organization's exports | platform:read |
|
POST /v1/platform/organizations/{org_id}/exports |
Export an organization's data | platform:provision |
required |
GET /v1/platform/organizations/{org_id}/exports/{run_id} |
Get an organization's export | platform:read |
|
GET /v1/platform/organizations/{org_id}/exports/{run_id}/download |
Download an organization's export | platform:provision |
|
GET /v1/platform/organizations/{org_id}/exports/{run_id}/link |
Get a link to download an organization's export | platform:provision |
|
POST /v1/platform/organizations/{org_id}/mark-demo |
Mark an organization as a demo | platform:provision |
|
POST /v1/platform/organizations/{org_id}/passkey-reset |
Reset a person's passkeys | platform:provision |
|
POST /v1/platform/organizations/{org_id}/reactivate |
Reactivate an organization | platform:provision |
|
POST /v1/platform/organizations/{org_id}/suspend |
Suspend an organization | platform:provision |
|
POST /v1/platform/organizations/{org_id}/undo-delete |
Undo the deletion of an organization | platform:provision |
GET /v1/platform/audit
List the operator's audit record
What the operator did and who tried, newest first: sign-ins (and refused ones), sign-outs,
provisioning, suspension, reactivation, marking a demo (and refusals), inviting an
administrator, opening an organization, and purging a demo. Each row names who (an operator's
address or a platform key), from which client address, which organization and the outcome.
The record is append-only. 404 for an unknown before. Needs the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | list_audit_v1_platform_audit_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | org_id |
string (uuid) or null | Only the record of this organization; omit for everything. | |
| query | limit |
integer | Most rows returned, 1 to 500. (min 1; max 500; default 100) |
|
| query | before |
string (uuid) or null | Continue after this row (its id, the last of the previous page); omit for the newest. |
Example request
curl "$ERP_URL/v1/platform/audit" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of PlatformAuditOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"at": "2026-09-21T09:30:00Z",
"action": "string",
"outcome": "denied",
"actor": {},
"address": "string",
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"org_name": "string",
"detail": {}
}
]
POST /v1/platform/auth/google
Sign in to the operator portal with Google
Exchange a Google ID token for an operator session cookie. For the operator portal
(/admin), not for scripts: a script uses a platform key.
The token is verified as the web UI's is (signature, audience, expiry, verified email), and
the account must also belong to its own Google Workspace (the hd claim is the address's
domain), be of an allowed domain when the deployment limits them, and be on the list of
platform operators (ERP_PLATFORM_OPERATORS). On success the cookie erp_platform_session is
set (HttpOnly, SameSite=Strict, path /v1/platform; Secure on https and in the cloud): it
lasts 8 hours and ends after 30 idle minutes, and it authenticates /v1/platform and nothing
else. Every attempt, successful or refused, is recorded in the platform audit. Needs the
X-ERP-UI: 1 header (403 csrf without it). Refused with 403 for a token that does not
verify or an account that is not an operator; 422 when Google sign-in is not configured.
| Required scope | any valid credential (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | operator_google_login_v1_platform_auth_google_post |
Request body
application/json (required): OperatorGoogleLogin
| Field | Type | Required | Description |
|---|---|---|---|
credential |
string | yes | Google ID token from Google Identity Services. (min length 1) |
Example request
curl -X POST "$ERP_URL/v1/platform/auth/google" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"credential": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij..."
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OperatorSignedIn |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"status": "signed_in",
"email": "string",
"expires_at": "2026-09-21T09:30:00Z"
}
POST /v1/platform/auth/logout
Sign out of the operator portal
End the operator session whose cookie accompanies the request (it is revoked, not only
forgotten) and clear the cookie; recorded in the platform audit. With a platform key there is
no session and nothing happens. The response has no body. Needs platform:read and, for a
session, the X-ERP-UI: 1 header.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | operator_logout_v1_platform_auth_logout_post |
Example request
curl -X POST "$ERP_URL/v1/platform/auth/logout" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 204 | No content | |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
GET /v1/platform/charts
List chart templates
The chart-of-accounts templates a new entity can start from, the value of chart in
POST /v1/platform/organizations. With country, the country's standard chart is marked
standard: the one an entity of that country gets when the request names no chart (the
Swiss KMU chart for CH and LI, the German HGB chart for DE, the French, Italian and Spanish
national charts for FR, IT and ES, the generic IFRS chart for a country without one of its
own). Needs
the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | list_charts_v1_platform_charts_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | country |
string or null | ISO country code: marks the chart an entity of that country starts from (standard). (pattern ^[A-Z]{2}$) |
Example request
curl "$ERP_URL/v1/platform/charts" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of ChartTemplate |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "string",
"name": "string",
"countries": [
"string"
],
"standard": false
}
]
GET /v1/platform/health
Service health: SLOs, error budget, latency, uptime and deploy
How the two services are doing against their objectives, for the operator's console. Per
service (erp-api, datavnext-api): each service level objective with its internal target,
its compliance over the rolling 28 days and the share of its error budget still unspent
(availability: requests that are not a 5xx, among the requests of real callers; latency: the
share of reads or writes the application answered under a threshold; the objectives and
thresholds are defined in Terraform and named in each entry), the 95th percentile of the last
24 hours by request class (read, report, write, heavy), and the version, revision and
time of the last deploy. Also the synthetic uptime checks (probes from outside, every five
minutes).
The figures come from Cloud Monitoring and Cloud Run, read with the service's own identity,
and are cached for 60 seconds. A part that cannot be read is named in problems and the rest
is still answered (status is then degraded; unavailable when nothing could be read,
which is also the answer where monitoring is not configured). Counts and durations of
requests only: no organization, person or amount. Needs the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | service_health_v1_platform_health_get |
Example request
curl "$ERP_URL/v1/platform/health" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ServiceHealth |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
Example response (shape)
{
"generated_at": "2026-09-21T09:30:00Z",
"status": "degraded",
"services": [
{
"id": "string",
"name": "string",
"slos": [
null
],
"latency_p95_ms_24h": {},
"deploy": null
}
],
"uptime_checks": [
{
"name": "string",
"url": "string",
"status": "down",
"success_ratio_15m": 1.5,
"success_ratio_24h": 1.5
}
],
"problems": [
{
"section": "string",
"reason": "string"
}
]
}
GET /v1/platform/me
Who the platform credential is
The operator behind the request: operator:<address> for a session of the operator
portal (with the address as email), platform:<prefix> for a platform key. The portal asks
it to learn whether the session is still valid; it ends after session_hours from sign-in
and after idle_minutes without a request. workspace_domains are the deployment's own
Google Workspace domains, whose people sign in with Google: the portal warns when a customer's
administrator is invited on one. Needs the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | whoami_v1_platform_me_get |
Example request
curl "$ERP_URL/v1/platform/me" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OperatorMe |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
Example response (shape)
{
"id": "string",
"name": "string",
"email": "string",
"via": "key",
"scopes": [
"string"
],
"session_hours": 1,
"idle_minutes": 1,
"workspace_domains": [
"string"
]
}
GET /v1/platform/models
Show the models behind the built-in agents
The models the built-in agents (bill capture, reconciliation, close review, Q&A) ask, in
order: the first answers unless it fails or does not answer within two minutes, then the next
(a refusal is an answer: no other model is asked). source says whether they are the
operator's latest choice or the deployment's defaults. available lists the Gemini models
Vertex AI offers at the region now, so that a newer one can be chosen without a deploy (the
list is cached for ten minutes; Claude models are not listed by Google). history is the
latest choices. Needs the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | get_models_v1_platform_models_get |
Example request
curl "$ERP_URL/v1/platform/models" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ModelSettings |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
Example response (shape)
{
"models": [
"string"
],
"source": "deployment",
"last_resort": [
"string"
],
"not_listed": [
"string"
],
"deployment_default": [
"string"
],
"region": "string",
"configured": true,
"available": [
"string"
],
"available_error": "string",
"history": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"models": [
"string"
],
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"reason": "string"
}
]
}
PUT /v1/platform/models
Choose the models behind the built-in agents
Choose the models the built-in agents ask, in order (1 to 4 Vertex AI model ids of the
Gemini or Claude family, each once). They are used from the next agent run on, for every
organization, and replace the deployment's defaults; a Claude model must be enabled in
Model Garden first. Check a model with POST /v1/platform/models/check before choosing it.
A choice is never changed or deleted: a new one replaces it, and each is recorded in the
platform audit (platform.choose_models, with the previous models). Only an operator signed
in to the portal may: a platform key is refused with 403 (and recorded). 422 for an id that
is not a Gemini or Claude model id, or one named twice. Needs the platform:provision
scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | choose_models_v1_platform_models_put |
Request body
application/json (required): ModelChoiceInput
| Field | Type | Required | Description |
|---|---|---|---|
models |
array of string | yes | The models to ask, in order: the first answers unless it fails or does not answer, then the next. 1 to 4 ids, each once. (min items 1; max items 4) |
reason |
string or null | Why, for the record (1 to 500 characters). (min length 1; max length 500) |
Example request
curl -X PUT "$ERP_URL/v1/platform/models" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"models": [
"gemini-3.8-flash",
"gemini-3.7-flash"
],
"reason": "3.7 as the fallback while 3.8 is in preview"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ModelChoiceOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"models": [
"string"
],
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"reason": "string"
}
POST /v1/platform/models/check
Check that a model answers
Ask one model, without fallback, a short test question at the configured Vertex AI
region, and say whether it answered and how long it took. No organization's data is sent. The
check is recorded in the platform audit (platform.check_model, outcome ok or failed).
A model that is unknown, not enabled or not served at the region answers ok: false with
the provider's error. 503 when no Google Cloud project is configured. Needs the
platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | check_model_v1_platform_models_check_post |
Request body
application/json (required): ModelCheckInput
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | yes | A Vertex AI model id of the Gemini or Claude family, e.g. gemini-3.8-flash or claude-opus-5-5. (pattern `^(gemini |
Example request
curl -X POST "$ERP_URL/v1/platform/models/check" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "gemini-3.8-flash"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ModelCheck |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"model": "string",
"ok": true,
"seconds": 1.5,
"error": "string"
}
GET /v1/platform/organizations
List organizations
The organizations on the platform, oldest first, with their lifecycle state (active
or suspended), their kind (customer or demo), when and by whom each was created and
why it was last suspended or reactivated. With usage=true each also has the number of
entities, people, journal entries and documents and when it was last used (that read is
recorded in the platform audit as org.list_usage). Never any accounting data beyond counts.
Pages of limit: pass the last org_id as after for the next one. Needs the
platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | list_organizations_v1_platform_organizations_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| query | status |
"active" | "suspended" or null |
Only active or only suspended organizations; omit for all. |
|
| query | kind |
"customer" | "demo" or null |
Only customer or only demo organizations; omit for all. |
|
| query | usage |
boolean | Add each organization's counts and last activity (usage). Each one is read in a transaction of its own, so ask for it with a page that you need. (default false) |
|
| query | limit |
integer | Most organizations returned, 1 to 500. (min 1; max 500; default 100) |
|
| query | after |
string (uuid) or null | Continue after this organization (its id, the last of the previous page); omit for the first page. |
Example request
curl "$ERP_URL/v1/platform/organizations" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of OrganizationSummary |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z",
"usage": {
"entities": 1,
"users": 1,
"entries": 1,
"documents": 1,
"last_activity_at": null,
"last_sign_in_at": null
}
}
]
POST /v1/platform/organizations
Provision an organization
Create an organization with its entities and invite its first people, in one step.
Each entity starts from a template (ch_sme, de_sme or generic: country, functional
currency, chart of accounts, VAT method, fiscal calendar), which the request can override.
The chart brings every account of the roles the services need. The first administrator
(admin) and any users are invited by email. When password sign-in is configured, Google
e-mails each of them the link that sets their password (password_email_sent), except to an
address of the deployment's Google Workspace domains, whose people sign in with Google. The
e-mails go out last, once everything is written; if Google refuses (502), nothing is created
and the request can be repeated. No key is created; the administrator creates their own
API key after the first sign-in, and it is bound to them.
All or nothing: if anything fails (an unknown chart, an entity code twice, an address
invited twice) nothing is created. A retry with the same Idempotency-Key and body returns
the stored response; the same key with another body answers 409. Needs the
platform:provision scope. The creation is an audit event of the new organization
(org.create, entity.create, user.invite, by platform:<prefix> or, from the operator
portal, operator:<address>), a security event (platform.organization.provision) and a row
of the platform audit. replayed is true when the answer is the stored one of an earlier
request with the same key. Refused: 401 without a valid platform key or operator session, 403
without the scope, 409 for a reused key or an entity code or address twice, 422 for an
unknown chart or a template without country or currency.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | required |
| Operation id | provision_organization_v1_platform_organizations_post |
Request body
application/json (required): ProvisionInput
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The organization's name, 1 to 200 characters. (min length 1; max length 200) |
admin |
ProvisionAdmin |
yes | The first administrator, who is invited with every scope and signs in with a password or Google. The organization has no API key: it creates its own, bound to this person, after the first sign-in. |
users |
array of ProvisionUser |
Further people to invite, up to 20. Invite a controller here: new vendors and the people's work need a second person (four eyes), so the administrator cannot complete the onboarding alone. (max items 20) |
|
entities |
array of ProvisionEntity |
The legal entities to create, each from a template. Default: none, and the first administrator sets the organization up at their first sign-in (POST /v1/setup): the country, accounting standard, currency, fiscal year and tax, then Production and its Test environment are made (ADR-0041). (max items 20) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"admin": {
"email": "petra.muster@example.com",
"name": "Petra Muster"
},
"entities": [
{
"code": "CH01",
"fiscal_years": [
2026
],
"name": "Muster Treuhand AG",
"template": "ch_sme",
"vat_number": "CHE-123.456.788 MWST"
}
],
"name": "Muster Treuhand AG",
"users": [
{
"email": "hans.keller@example.com",
"name": "Hans Keller"
}
]
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 201 | Created | Provisioned |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"organization": {
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
},
"entities": [
{
"code": "string",
"name": "string",
"country": "string",
"functional_currency": "string",
"chart": "string",
"role_accounts": 1,
"fiscal_years": [
1
],
"vat_method": "string"
}
],
"users": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"scopes": [
"string"
],
"status": "string",
"password_email_sent": false
}
],
"replayed": false
}
GET /v1/platform/organizations/{org_id}
Get an organization
One organization as the operator sees it: its lifecycle state and kind, its entities
(code, name, country, currency), its people (address, role, status, last sign-in), its counts
and the evidence of use that decides whether it may be marked as a demo. Master data and
counts only: no ledger line, amount or document. Opening an organization is recorded in the
platform audit (org.view). 404 for an unknown id. Needs the platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | get_organization_v1_platform_organizations__org_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Example request
curl "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OrganizationDetail |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z",
"usage": {
"entities": 1,
"users": 1,
"entries": 1,
"documents": 1,
"last_activity_at": "2026-09-21T09:30:00Z",
"last_sign_in_at": "2026-09-21T09:30:00Z"
},
"entities": [
{
"code": "string",
"name": "string",
"country": "string",
"functional_currency": "string"
}
],
"users": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"name": "string",
"role": "string",
"status": "string",
"created_at": "2026-09-21T09:30:00Z",
"last_login_at": "2026-09-21T09:30:00Z"
}
],
"evidence": {
"users": 1,
"users_not_administrators": 1,
"users_invited_by_people": 1,
"users_signed_in": 1,
"journal_entries": 1,
"journal_entries_not_by_the_seed": 1,
"documents": 1,
"documents_not_by_the_seed": 1,
"document_dispatches": 1,
"issued_outputs_archived": 1,
"bank_statements": 1,
"first_posting": "2026-09-21",
"last_posting": "2026-09-21",
"customer_grade": [
"string"
]
}
}
POST /v1/platform/organizations/{org_id}/admins
Invite or show again an organization's first administrator
Invite an administrator of an existing organization, who signs in with the Google account
of the address or, without one, with a password. A person who is not a user yet is added
(invited); one who was invited and has not signed in is left as they are
(already_invited) and the answer says again where to sign in. When password sign-in is
configured, Google e-mails them the link that sets their password, a fresh one each time
(password_email_sent; not to an address of the deployment's Google Workspace domains;
502 when Google refuses). Both are recorded in the platform
audit; a new invitation is also an event of the organization's audit trail. Safe to repeat.
409 for a person who has signed in, who is disabled or who was invited with another role;
404 for an unknown organization. Needs the platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | invite_admin_v1_platform_organizations__org_id__admins_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): AdminInvite
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The administrator's address; they sign in with the Google account of it, or, without one, with the address and a password. (pattern ^[^@\s]+@[^@\s]+\.[^@\s]+$) |
name |
string or null | Display name. (max length 200) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/admins" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "petra.muster@example.com",
"name": "Petra Muster"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | AdminInvited |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"user": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"email": "string",
"scopes": [
"string"
],
"status": "string",
"password_email_sent": false
},
"result": "already_invited",
"sign_in_path": "string",
"password_email_sent": false
}
POST /v1/platform/organizations/{org_id}/delete
Delete an organization
Delete an organization and its test copies, with everything they hold (ADR-0042). Only an
operator signed in to the portal may: a platform key is refused with 403. confirm_name must
be the organization's exact name (422 otherwise). An organization that shows signs of real use
(the evidence of its detail) is refused with 409 customer_activity unless accept_activity
is true. The answer is 202: from now on nobody reaches the organization. For an hour
(until delete_after) the deletion can be undone (undo-delete); after that the owner's job,
which runs every hour and at every deploy, deletes it with the demo purge over the owner
connection (ADR-0022) and records how many rows it deleted, and nothing brings its books back
but a restore of the database. Asking again changes nothing. The request and a refusal are
recorded in the platform audit. 404 for an unknown id. Needs the platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | delete_organization_v1_platform_organizations__org_id__delete_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): DeleteOrganizationInput
| Field | Type | Required | Description |
|---|---|---|---|
confirm_name |
string | yes | The organization's exact name, as it is written (no trimming, no case folding): a guard against deleting the wrong one. (min length 1; max length 200) |
accept_activity |
boolean | Delete it although it shows signs of real use (the evidence of the organization's detail, customer_grade): its books are deleted with it. Without it, such an organization is refused with 409 customer_activity. (default false) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/delete" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"confirm_name": "Muster Treuhand AG"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 202 | Successful Response | DeleteOrganizationResult |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"organization": {
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
},
"changed": true,
"evidence": {
"users": 1,
"users_not_administrators": 1,
"users_invited_by_people": 1,
"users_signed_in": 1,
"journal_entries": 1,
"journal_entries_not_by_the_seed": 1,
"documents": 1,
"documents_not_by_the_seed": 1,
"document_dispatches": 1,
"issued_outputs_archived": 1,
"bank_statements": 1,
"first_posting": "2026-09-21",
"last_posting": "2026-09-21",
"customer_grade": [
"string"
]
}
}
GET /v1/platform/organizations/{org_id}/exports
List an organization's exports
The organization's exports, newest first, with their status, whoever asked for them.
Pages of limit: pass the last id as after for the next one. Needs the platform:read
scope. 404 for an unknown organization.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | list_organization_exports_v1_platform_organizations__org_id__exports_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
| query | limit |
integer | Most exports returned, 1 to 100. (min 1; max 100; default 25) |
|
| query | after |
string (uuid) or null | Continue after this export (its id, the last of the previous page); omit for the first page. |
Example request
curl "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/exports" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | array of ExportOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "expired",
"via": "platform",
"language": "de",
"note": "string",
"requested_by": {},
"requested_at": "2026-09-21T09:30:00Z",
"started_at": "2026-09-21T09:30:00Z",
"finished_at": "2026-09-21T09:30:00Z",
"stage": "string",
"progress": {},
"error": "string",
"cutoff": "2026-09-21T09:30:00Z",
"file_name": "string",
"size_bytes": 1,
"sha256": "string",
"manifest_sha256": "string",
"file_count": 1,
"row_count": 1,
"complete": true,
"statement": {},
"expires_at": "2026-09-21T09:30:00Z"
}
]
POST /v1/platform/organizations/{org_id}/exports
Export an organization's data
Ask for the complete package of an organization's data, as the organization's own
administrators can (POST /v1/exports): every table as CSV and JSON, the reports as PDF and
CSV, every stored file checked against its SHA-256, the hash-chain verification, a manifest
with the hash of every file, the README, and for a German entity the index.xml. Meant for
offboarding: suspend the organization first (POST .../suspend), then export, then hand
over. The export runs asynchronously from one consistent snapshot; follow it with
GET .../exports/{id}.
The platform reads none of the books through this API: the job that builds the package does,
over the database's row-level security, scoped to the one organization. When it has
succeeded, the SHA-256 of the package is recorded in the platform's append-only audit record
(platform_audit, action org.export), so that we can later attest what was handed over.
One export at a time per organization (409 export_in_progress). Needs the
platform:provision scope. Starting is an audit event of the organization (export.start,
by platform:<prefix>), a security event (platform.export.start) and a row of the
operator's record (org.export_start). 404 for an unknown
organization. A retry with the same Idempotency-Key returns the first answer.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | required |
| Operation id | start_organization_export_v1_platform_organizations__org_id__exports_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): ExportRequest
| Field | Type | Required | Description |
|---|---|---|---|
language |
"de" | "en" | "fr" | "it" | "sv" or null |
The language of the package's README: en, de, fr, it or sv. Default: the language of the person who asks, else the country of the organization's first entity (de for CH and DE, fr for FR, it for IT, sv for SE), else en. |
|
note |
string or null | Why the export is made, up to 500 characters; kept with the request and in the audit trail. (max length 500) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/exports" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"language": "de",
"note": "Archive copy at contract end"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 202 | Successful Response | ExportOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "expired",
"via": "platform",
"language": "de",
"note": "string",
"requested_by": {},
"requested_at": "2026-09-21T09:30:00Z",
"started_at": "2026-09-21T09:30:00Z",
"finished_at": "2026-09-21T09:30:00Z",
"stage": "string",
"progress": {},
"error": "string",
"cutoff": "2026-09-21T09:30:00Z",
"file_name": "string",
"size_bytes": 1,
"sha256": "string",
"manifest_sha256": "string",
"file_count": 1,
"row_count": 1,
"complete": true,
"statement": {},
"expires_at": "2026-09-21T09:30:00Z"
}
GET /v1/platform/organizations/{org_id}/exports/{run_id}
Get an organization's export
One export with its status and, once it has succeeded, the file's SHA-256 and the
hand-over statement. 404 for an unknown organization or an export of another one. Needs the
platform:read scope.
| Required scope | platform:read (a platform key; no tenant credential works here) |
| Operation id | get_organization_export_v1_platform_organizations__org_id__exports__run_id__get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
| path | run_id |
string (uuid) | yes | Id of an export (GET /v1/exports). |
Example request
curl "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/exports/3fa85f64-5717-4562-b3fc-2c963f66afa6" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ExportOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "expired",
"via": "platform",
"language": "de",
"note": "string",
"requested_by": {},
"requested_at": "2026-09-21T09:30:00Z",
"started_at": "2026-09-21T09:30:00Z",
"finished_at": "2026-09-21T09:30:00Z",
"stage": "string",
"progress": {},
"error": "string",
"cutoff": "2026-09-21T09:30:00Z",
"file_name": "string",
"size_bytes": 1,
"sha256": "string",
"manifest_sha256": "string",
"file_count": 1,
"row_count": 1,
"complete": true,
"statement": {},
"expires_at": "2026-09-21T09:30:00Z"
}
GET /v1/platform/organizations/{org_id}/exports/{run_id}/download
Download an organization's export
The package of a succeeded export, as a ZIP, streamed through the API (use a client that
writes to a file; for a large package prefer the link). Every download is an audit event of
the organization (export.download), a security event (platform.export.download) and a row
of the operator's record (org.export_download).
Needs the platform:provision scope. 409 export_not_ready unless the export succeeded and
has not expired.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Operation id | download_organization_export_v1_platform_organizations__org_id__exports__run_id__download_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
| path | run_id |
string (uuid) | yes | Id of an export (GET /v1/exports). |
Example request
curl "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/exports/3fa85f64-5717-4562-b3fc-2c963f66afa6/download" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | The package: a ZIP, streamed (SHA-256 in X-Content-SHA256). |
application/zip |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
GET /v1/platform/organizations/{org_id}/exports/{run_id}/link
Get a link to download an organization's export
A signed link that downloads the package straight from the store without credentials,
for seven days at most and never beyond the package's expiry: this is how the package is
handed over to the customer, who has no platform key. Anyone who holds the link can download
until it ends: send it over a channel you trust, together with the statement
(GET .../exports/{id}: SHA-256 of the file). Giving the link is an audit event
(export.link), a security event (platform.export.download) and a row of the operator's
record (org.export_link). Needs the
platform:provision scope. 409 export_not_ready unless the export succeeded and has not
expired, 409 no_signed_link where links cannot be signed (use /download).
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Operation id | organization_export_link_v1_platform_organizations__org_id__exports__run_id__link_get |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
| path | run_id |
string (uuid) | yes | Id of an export (GET /v1/exports). |
Example request
curl "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/exports/3fa85f64-5717-4562-b3fc-2c963f66afa6/link" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | ExportLink |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"url": "string",
"expires_at": "2026-09-21T09:30:00Z",
"file_name": "string",
"size_bytes": 1,
"sha256": "string"
}
POST /v1/platform/organizations/{org_id}/mark-demo
Mark an organization as a demo
Make an organization a demo: the one kind the operator may purge (with a command, never
through this API; a customer's books are never deleted). Only an operator signed in to the
portal may: a platform key is refused with 403. confirm_name must be the organization's exact
name (422 otherwise). It is done by a database function that also checks the evidence of
use: an organization that shows any sign of real use (an invoice sent to a customer, a file
archived as issued, a person who is not an administrator or was invited by a person or a key,
more than one person who has signed in, entries or documents not created by the demo seed) is
refused with 409 customer_activity, and the evidence is in the answer. There is no way to
accept it here: an organization that was used is marked only by the command erp platform mark-demo --accept-activity on the owner connection. A marking and a refusal are recorded in
the platform audit. Marking a demo again changes and records nothing. 404 for an unknown id.
Needs the platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | mark_demo_v1_platform_organizations__org_id__mark_demo_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): MarkDemoInput
| Field | Type | Required | Description |
|---|---|---|---|
confirm_name |
string | yes | The organization's exact name, as it is written (no trimming, no case folding): a guard against marking the wrong one. (min length 1; max length 200) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/mark-demo" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"confirm_name": "Muster Treuhand AG"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | MarkDemoResult |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"organization": {
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
},
"changed": true,
"evidence": {
"users": 1,
"users_not_administrators": 1,
"users_invited_by_people": 1,
"users_signed_in": 1,
"journal_entries": 1,
"journal_entries_not_by_the_seed": 1,
"documents": 1,
"documents_not_by_the_seed": 1,
"document_dispatches": 1,
"issued_outputs_archived": 1,
"bank_statements": 1,
"first_posting": "2026-09-21",
"last_posting": "2026-09-21",
"customer_grade": [
"string"
]
}
}
POST /v1/platform/organizations/{org_id}/passkey-reset
Reset a person's passkeys
For a person of the organization who lost every device with a passkey and has no
administrator to ask, such as a company's only owner: remove their passkeys and end all
their sessions, in every organization; their next sign-in creates a new first passkey.
Only an operator signed in to the portal (403 for a platform key), with the
platform:provision scope. 404 when the address is not a user of the organization.
Recorded in the platform audit (org.passkey_reset), the organization's audit trail
(user.passkey_reset) and as a security event (passkey.reset).
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | operator_reset_passkeys_v1_platform_organizations__org_id__passkey_reset_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): OperatorPasskeyReset
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The address of the person whose passkeys to reset. |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/passkey-reset" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "erin@example.com"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | OperatorPasskeyResetDone |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"email": "string",
"removed": 1
}
POST /v1/platform/organizations/{org_id}/reactivate
Reactivate an organization
Lift a suspension: the organization can change data again. The reason goes to the
audit trail (org.reactivate) and to a security event. Safe to repeat: an organization
that is already active is returned unchanged. 404 for an unknown id. Needs the
platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | reactivate_organization_v1_platform_organizations__org_id__reactivate_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): StatusChange
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why the organization is suspended or reactivated, 3 to 500 characters; kept in the organization's audit trail and in the platform's security events. (min length 3; max length 500) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/reactivate" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Invoice 2026-114 unpaid after two reminders"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | TenantOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
}
POST /v1/platform/organizations/{org_id}/suspend
Suspend an organization
Make an organization read-only. From now on its API keys and sessions can read
everything and change nothing: every request that would change state answers 403
(suspended); signing in and out still work. Nothing is deleted. The reason goes to
the organization's audit trail (org.suspend) and to a security event. Safe to repeat: an
organization that is already suspended is returned unchanged. 404 for an unknown id.
Needs the platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | suspend_organization_v1_platform_organizations__org_id__suspend_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Request body
application/json (required): StatusChange
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why the organization is suspended or reactivated, 3 to 500 characters; kept in the organization's audit trail and in the platform's security events. (min length 3; max length 500) |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/suspend" \
-H "Authorization: Bearer $PLATFORM_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Invoice 2026-114 unpaid after two reminders"
}'
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | TenantOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
}
POST /v1/platform/organizations/{org_id}/undo-delete
Undo the deletion of an organization
Undo a deletion asked for less than an hour ago (before delete_after): the
organization, and the test copies deleted with it, are reachable again as they were. Only an
operator signed in to the portal may: a platform key is refused with 403. After the hour the
owner's job may be deleting it: 409. Safe to repeat: an organization whose deletion was not
asked for is returned unchanged. Recorded in the platform audit (org.delete_undone). 404 for
an unknown id. Needs the platform:provision scope.
| Required scope | platform:provision (a platform key; no tenant credential works here) |
| Idempotency-Key | not used |
| Operation id | undo_delete_organization_v1_platform_organizations__org_id__undo_delete_post |
Parameters
| In | Name | Type | Required | Description |
|---|---|---|---|---|
| path | org_id |
string (uuid) | yes | Organization id (GET /v1/platform/organizations). |
Example request
curl -X POST "$ERP_URL/v1/platform/organizations/3fa85f64-5717-4562-b3fc-2c963f66afa6/undo-delete" \
-H "Authorization: Bearer $PLATFORM_KEY"
Responses
| Status | Meaning | Body |
|---|---|---|
| 200 | OK | TenantOut |
| 401 | Missing or invalid API key (unauthorized). |
ErrorEnvelope |
| 403 | The caller lacks the required scope (missing_right, with scope), the key belongs to no person where a person is needed (forbidden), or the approval policy denies the action (policy_denied, with action and rule_id). |
ErrorEnvelope |
| 404 | The resource does not exist, or belongs to another organization (not_found). |
ErrorEnvelope |
| 409 | The request conflicts with the data: an Idempotency-Key reused with a different request, a closed period, or an integrity rule of the database (conflict, integrity_violation). |
ErrorEnvelope |
| 422 | Validation failed: the error envelope (validation_failed). For a malformed body or parameter its message is in sentences ("Line 2: the quantity must be above zero") and fields lists each error with pydantic's loc, type and msg beside its sentence (message) and its parts, to word it in another language: items (name and number from 1: line 2), field (its name as sent) and the rule it breaks with its limit, count and choices (none for a validator's own sentence); FastAPI's own detail list stays beside the envelope. |
HTTPValidationError |
Example response (shape)
{
"org_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "string",
"status": "active",
"created_at": "2026-09-21T09:30:00Z",
"created_by": {},
"status_changed_at": "2026-09-21T09:30:00Z",
"status_reason": "string",
"kind": "customer",
"copy_of": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"remove_requested_at": "2026-09-21T09:30:00Z",
"delete_after": "2026-09-21T09:30:00Z"
}
Schemas
AdminInvite
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The administrator's address; they sign in with the Google account of it, or, without one, with the address and a password. (pattern ^[^@\s]+@[^@\s]+\.[^@\s]+$) |
name |
string or null | Display name. (max length 200) |
AdminInvited
| Field | Type | Required | Description |
|---|---|---|---|
user |
ProvisionedUser |
yes | The administrator, as invited. |
result |
"already_invited" | "invited" |
yes | invited: the person was just added. already_invited: they were invited before and have not signed in yet; nothing changed, and the act is recorded. |
sign_in_path |
string | yes | Where they sign in, on the host that serves the API (/app/). |
password_email_sent |
boolean | True when Google e-mailed them the link that sets a password (a fresh one for someone invited before); false when password sign-in is not configured, or for an address of a Google Workspace domain, whose people sign in with Google. (default false) |
DeleteOrganizationInput
| Field | Type | Required | Description |
|---|---|---|---|
confirm_name |
string | yes | The organization's exact name, as it is written (no trimming, no case folding): a guard against deleting the wrong one. (min length 1; max length 200) |
accept_activity |
boolean | Delete it although it shows signs of real use (the evidence of the organization's detail, customer_grade): its books are deleted with it. Without it, such an organization is refused with 409 customer_activity. (default false) |
DeleteOrganizationResult
| Field | Type | Required | Description |
|---|---|---|---|
organization |
TenantOut |
yes | The organization, with remove_requested_at set: deleted for good after delete_after, and until then the deletion can be undone. |
changed |
boolean | yes | False when its deletion was asked for already. |
evidence |
OrganizationEvidence |
yes | The evidence of use that was measured. |
DeployOut
What the Cloud Run service runs.
| Field | Type | Required | Description |
|---|---|---|---|
version |
string or null | yes | The image tag of the latest revision: the first 12 characters of the commit. |
latest_revision |
string or null | yes | The latest ready revision. |
serving_revision |
string or null | yes | The revision with the largest traffic share. It differs from latest_revision during a canary and after a rollback. |
updated_at |
string (date-time) or null | yes | When the service last changed: a deploy, a rollback or a settings change (ISO 8601, UTC). |
EntitySummary
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | The entity's code. |
name |
string | yes | Legal name. |
country |
string | yes | ISO 3166-1 alpha-2 country code. |
functional_currency |
string | yes | ISO 4217 code of the books' currency. |
MarkDemoInput
| Field | Type | Required | Description |
|---|---|---|---|
confirm_name |
string | yes | The organization's exact name, as it is written (no trimming, no case folding): a guard against marking the wrong one. (min length 1; max length 200) |
MarkDemoResult
| Field | Type | Required | Description |
|---|---|---|---|
organization |
TenantOut |
yes | The organization, now of kind demo. |
changed |
boolean | yes | False when it already was a demo: nothing was changed. |
evidence |
OrganizationEvidence |
yes | The evidence of use that was measured. |
ModelCheck
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | yes | The model asked. |
ok |
boolean | yes | True when the model answered a short test request. |
seconds |
number | yes | How long it took to answer or fail. |
error |
string or null | yes | What went wrong, when it did not answer. |
ModelCheckInput
| Field | Type | Required | Description |
|---|---|---|---|
model |
string | yes | A Vertex AI model id of the Gemini or Claude family, e.g. gemini-3.8-flash or claude-opus-5-5. (pattern `^(gemini |
ModelChoiceInput
| Field | Type | Required | Description |
|---|---|---|---|
models |
array of string | yes | The models to ask, in order: the first answers unless it fails or does not answer, then the next. 1 to 4 ids, each once. (min items 1; max items 4) |
reason |
string or null | Why, for the record (1 to 500 characters). (min length 1; max length 500) |
ModelChoiceOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id (uuid) of the choice. |
models |
array of string | yes | The models, in the order they are asked. |
created_at |
string (date-time) | yes | When it was chosen (ISO 8601, UTC). |
created_by |
object | yes | Who chose it: type, id, name, email. |
reason |
string or null | yes | Why, as given. |
ModelSettings
| Field | Type | Required | Description |
|---|---|---|---|
models |
array of string | yes | The models the built-in agents ask now, in order (the first, then each fallback). |
source |
"deployment" | "operator" |
yes | operator: the latest choice made here; deployment: none was made, so the defaults of the deployment apply (ERP_LLM_PROVIDER, ERP_LLM_MODEL, ERP_LLM_FALLBACK_MODELS). |
last_resort |
array of string | yes | With an operator's choice, the deployment's defaults asked after it when every chosen model fails, so that an id Vertex AI does not serve cannot stop the agents. |
not_listed |
array of string | yes | Chosen Gemini models that are not in available: possibly a typo or a retired model. Empty when the list could not be read. |
deployment_default |
array of string | yes | The deployment's defaults, which apply when no choice was made. |
region |
string | yes | The Vertex AI location the models are called at. |
configured |
boolean | yes | False when no Google Cloud project is configured: the agents answer 503. |
available |
array of string | yes | The Gemini text models Google lists for the project at region now (embedding models left out). Claude models are not listed by Google: enable one in Model Garden, then enter its id. |
available_error |
string or null | yes | Why the list of available models could not be read, if it could not. |
history |
array of ModelChoiceOut |
yes | The latest choices, newest first. |
OperatorGoogleLogin
| Field | Type | Required | Description |
|---|---|---|---|
credential |
string | yes | Google ID token from Google Identity Services. (min length 1) |
OperatorMe
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | operator:<address> of a signed-in operator, platform:<prefix> of a key. |
name |
string | yes | The operator's address, or the key's name. |
email |
string or null | yes | The operator's address; null for a platform key. |
via |
"key" | "session" |
yes | What authenticated the request. |
scopes |
array of string | yes | The platform scopes it holds. |
session_hours |
integer | yes | How long a session lasts from sign-in. |
idle_minutes |
integer | yes | How long a session may sit idle before it ends. |
workspace_domains |
array of string | The deployment's own Google Workspace domains: their people sign in with Google and never get a password, so an address there is no customer's administrator. |
OperatorPasskeyReset
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The address of the person whose passkeys to reset. |
OperatorPasskeyResetDone
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The person whose passkeys were reset. |
removed |
integer | yes | How many passkeys were removed. |
OperatorSignedIn
| Field | Type | Required | Description |
|---|---|---|---|
status |
"signed_in" |
yes | signed_in: the erp_platform_session cookie was set. |
email |
string | yes | The operator's address. |
expires_at |
string (date-time) | yes | When the session ends at the latest (ISO 8601, UTC). |
OrganizationDetail
One organization as the operator sees it: master data and counts, never the books.
| Field | Type | Required | Description |
|---|---|---|---|
org_id |
string (uuid) | yes | Id (uuid) of the organization. |
name |
string | yes | The organization's name. |
status |
"active" | "suspended" |
yes | active, or suspended: the organization can read but not change anything until it is reactivated. |
created_at |
string (date-time) | yes | When the organization was created (ISO 8601, UTC). |
created_by |
object | yes | Who created it, as an actor object (type, id, name): a platform key (platform:<prefix>) or the command line. |
status_changed_at |
string (date-time) or null | yes | When it was last suspended or reactivated; null if never. |
status_reason |
string or null | yes | Why it was last suspended or reactivated; null if never. |
kind |
"customer" | "demo" |
customer: its books are kept for the statutory period and never deleted. demo: made to be looked at. An operator deletes either in the portal (POST /v1/platform/organizations/{org_id}/delete, ADR-0042). (default "customer") |
|
copy_of |
string (uuid) or null | For a test copy (always a demo), the organization it was made from (ADR-0039); null for every other organization. |
|
remove_requested_at |
string (date-time) or null | When its deletion was asked for (POST /v1/platform/organizations/{org_id}/delete, or the removal of a test copy): nobody reaches it from then on, and an operator can undo it until delete_after (ADR-0042). Null otherwise. |
|
delete_after |
string (date-time) or null | When the owner's job may delete it for good: an hour after remove_requested_at. It runs every hour and at every deploy, so the organization is gone within the hour after this. Until then an operator can undo the deletion (POST /v1/platform/organizations/{org_id}/undo-delete). Null when no deletion was asked for. |
|
usage |
OrganizationUsage or null |
Counts and last activity; null unless the list was asked with usage=true (each organization is read in a transaction of its own, so it is not free). |
|
entities |
array of EntitySummary |
yes | Its legal entities. |
users |
array of OrganizationUser |
yes | Its people, by address. |
evidence |
OrganizationEvidence |
yes | The evidence of use. |
OrganizationEvidence
The signs of real use that decide whether an organization may be marked as a demo
(org_activity, ADR-0022). A non-empty customer_grade means it looks like a customer.
| Field | Type | Required | Description |
|---|---|---|---|
users |
integer | yes | People of the organization. |
users_not_administrators |
integer | yes | People who are not administrators. |
users_invited_by_people |
integer | yes | People invited by a person or an API key, not by the demo seed, the system or the platform: the initial administrators are not counted. |
users_signed_in |
integer | yes | People who have signed in at least once. |
journal_entries |
integer | yes | Journal entries. |
journal_entries_not_by_the_seed |
integer | yes | Journal entries created by an actor that is neither demo:* nor system:*. |
documents |
integer | yes | Invoices and bills. |
documents_not_by_the_seed |
integer | yes | Invoices and bills created by such an actor. |
document_dispatches |
integer | yes | Invoices handed to a customer. |
issued_outputs_archived |
integer | yes | Files archived as issued. |
bank_statements |
integer | yes | Bank statements imported. |
first_posting |
string (date) or null | yes | Date of the first posted entry; null if none. |
last_posting |
string (date) or null | yes | Date of the last posted entry; null if none. |
customer_grade |
array of string | yes | Why it looks like a customer's organization; empty when it looks like a demo. |
OrganizationSummary
A row of the operator's list: the lifecycle, and the usage when it was asked for.
| Field | Type | Required | Description |
|---|---|---|---|
org_id |
string (uuid) | yes | Id (uuid) of the organization. |
name |
string | yes | The organization's name. |
status |
"active" | "suspended" |
yes | active, or suspended: the organization can read but not change anything until it is reactivated. |
created_at |
string (date-time) | yes | When the organization was created (ISO 8601, UTC). |
created_by |
object | yes | Who created it, as an actor object (type, id, name): a platform key (platform:<prefix>) or the command line. |
status_changed_at |
string (date-time) or null | yes | When it was last suspended or reactivated; null if never. |
status_reason |
string or null | yes | Why it was last suspended or reactivated; null if never. |
kind |
"customer" | "demo" |
customer: its books are kept for the statutory period and never deleted. demo: made to be looked at. An operator deletes either in the portal (POST /v1/platform/organizations/{org_id}/delete, ADR-0042). (default "customer") |
|
copy_of |
string (uuid) or null | For a test copy (always a demo), the organization it was made from (ADR-0039); null for every other organization. |
|
remove_requested_at |
string (date-time) or null | When its deletion was asked for (POST /v1/platform/organizations/{org_id}/delete, or the removal of a test copy): nobody reaches it from then on, and an operator can undo it until delete_after (ADR-0042). Null otherwise. |
|
delete_after |
string (date-time) or null | When the owner's job may delete it for good: an hour after remove_requested_at. It runs every hour and at every deploy, so the organization is gone within the hour after this. Until then an operator can undo the deletion (POST /v1/platform/organizations/{org_id}/undo-delete). Null when no deletion was asked for. |
|
usage |
OrganizationUsage or null |
Counts and last activity; null unless the list was asked with usage=true (each organization is read in a transaction of its own, so it is not free). |
OrganizationUsage
What an organization has been used for: counts only, never a ledger line or an amount.
| Field | Type | Required | Description |
|---|---|---|---|
entities |
integer | yes | Number of legal entities. |
users |
integer | yes | Number of people (invited, active or disabled). |
entries |
integer | yes | Number of journal entries, drafts and posted. |
documents |
integer | yes | Number of invoices, bills and credit notes. |
last_activity_at |
string (date-time) or null | yes | The last event in the organization's audit trail that a person, a key or the demo seed caused (what the platform does inside it does not count); null if none. |
last_sign_in_at |
string (date-time) or null | yes | The last sign-in of any of its people; null if nobody ever signed in. |
OrganizationUser
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id (uuid) of the user. |
email |
string | yes | The sign-in address, lower case. |
name |
string or null | yes | Display name; null until set. |
role |
string | yes | admin, controller, accountant, auditor or viewer when the scopes are exactly that preset, else custom. |
status |
string | yes | invited (never signed in), active or disabled. |
created_at |
string (date-time) | yes | When the user was invited (ISO 8601, UTC). |
last_login_at |
string (date-time) or null | yes | The last sign-in; null if never. |
PlatformAuditOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id (uuid) of the record. |
at |
string (date-time) | yes | When (ISO 8601, UTC). |
action |
string | yes | What was done: operator.sign_in, operator.sign_in_denied, operator.sign_out, org.provision, org.suspend, org.reactivate, org.mark_demo, org.mark_demo_refused, org.mark_customer, org.delete, org.delete_refused, org.delete_undone, org.purge_demo, org.invite_admin, org.reshow_invite, org.passkey_reset, org.view, platform.choose_models, platform.check_model. |
outcome |
"denied" | "failed" | "ok" | "refused" |
yes | ok, denied (not allowed to), refused (a guard said no) or failed. |
actor |
object | yes | Who: type, id and name, and email for an operator. |
address |
string or null | yes | The client address; null for the command line. |
org_id |
string (uuid) or null | yes | The organization it concerns, if any. |
org_name |
string or null | yes | Its name when it happened. |
detail |
object | yes | What else is worth knowing; never a secret. |
Problem
| Field | Type | Required | Description |
|---|---|---|---|
section |
string | yes | What could not be read, for example slo:erp-api. |
reason |
string | yes | Why, in a few words (the API's status code or request failed); never a response body. |
ProvisionAdmin
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The first administrator's email address. They sign in with a password whose link Google e-mails them when they are invited (when password sign-in is configured), or, for an address of the deployment's Google Workspace domains, with Google; the address is stored in lower case. (pattern ^[^@\s]+@[^@\s]+\.[^@\s]+$) |
name |
string or null | Display name; filled from Google at sign-in. (max length 200) |
ProvisionEntity
| Field | Type | Required | Description |
|---|---|---|---|
street |
string or null | Street of the entity's address, at most 70 characters. (max length 70) | |
building_number |
string or null | Building number of the address, at most 16 characters. (max length 16) | |
postal_code |
string or null | Postal code of the address, at most 16 characters. (max length 16) | |
town |
string or null | Town of the address, at most 35 characters. (max length 35) | |
contact_name |
string or null | Seller contact (BT-41). | |
contact_phone |
string or null | Seller contact phone (BT-42). | |
contact_email |
string or null | Seller contact email (BT-43). | |
electronic_address |
string or null | Seller electronic address (BT-34); defaults to contact_email. | |
electronic_address_scheme |
string or null | EAS code, e.g. EM (email), 9930 (German VAT id), 0204. | |
template |
"ch_sme" | "de_sme" | "generic" |
yes | The preset the entity starts from. ch_sme: Switzerland, CHF, monthly periods, VAT by the effective method per quarter. de_sme: Germany, EUR, monthly periods, VAT with the standard method per month and a monthly recapitulative statement. generic: no country or currency; send country and functional_currency. The chart is the standard chart of the entity's country (GET /v1/platform/charts: ch_kmu for CH and LI, de_hgb for DE, fr_pcg for FR, it_civil_code for IT, es_pgc for ES, se_bas for SE, fi_kpl for FI, dk_standard for DK, no_ns4102 for NO, ifrs_generic where a country has none). Every value of a preset can be overridden below. |
code |
string | yes | Short unique code, 2 to 20 characters of A-Z, 0-9, _ and -, e.g. CH01; it names the entity in every URL. (pattern ^[A-Z0-9_-]{2,20}$) |
name |
string | yes | Legal name of the entity. (min length 1) |
country |
string or null | ISO 3166-1 alpha-2 country code; default: the template's. (pattern ^[A-Z]{2}$) |
|
subdivision |
string or null | ISO 3166-2 code of the state or canton, e.g. DE-BY; null when omitted. (pattern ^[A-Z]{2}-[A-Z0-9]{1,3}$) |
|
functional_currency |
string or null | ISO 4217 code of the currency the books are kept in; default: the template's. Cannot be changed later. (pattern ^[A-Z]{3}$) |
|
chart |
string or null | Chart template id (GET /v1/platform/charts); default: the standard chart of the entity's country. |
|
fiscal_year_start_month |
integer | Month the fiscal year starts, 1 to 12; default 1. (min 1; max 12; default 1) |
|
vat_number |
string or null | The entity's VAT number; null when omitted. | |
fiscal_years |
array of integer | The fiscal years whose periods are opened at once, e.g. [2026], each 2000 to 2100; default: the one that holds today where the company is, on its fiscal_year_start_month. |
|
fiscal_calendar |
FiscalCalendar or null |
How to divide those years into periods; default: monthly. | |
vat |
VatSettingsInput or null |
VAT settings of the entity (method, period); default: the template's. generic has none, so the country's defaults apply until they are saved. |
ProvisionInput
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The organization's name, 1 to 200 characters. (min length 1; max length 200) |
admin |
ProvisionAdmin |
yes | The first administrator, who is invited with every scope and signs in with a password or Google. The organization has no API key: it creates its own, bound to this person, after the first sign-in. |
users |
array of ProvisionUser |
Further people to invite, up to 20. Invite a controller here: new vendors and the people's work need a second person (four eyes), so the administrator cannot complete the onboarding alone. (max items 20) |
|
entities |
array of ProvisionEntity |
The legal entities to create, each from a template. Default: none, and the first administrator sets the organization up at their first sign-in (POST /v1/setup): the country, accounting standard, currency, fiscal year and tax, then Production and its Test environment are made (ADR-0041). (max items 20) |
ProvisionUser
| Field | Type | Required | Description |
|---|---|---|---|
email |
string | yes | The first administrator's email address. They sign in with a password whose link Google e-mails them when they are invited (when password sign-in is configured), or, for an address of the deployment's Google Workspace domains, with Google; the address is stored in lower case. (pattern ^[^@\s]+@[^@\s]+\.[^@\s]+$) |
name |
string or null | Display name; filled from Google at sign-in. (max length 200) | |
role |
"accountant" | "admin" | "auditor" | "controller" | "viewer" |
Preset scopes of the user: viewer, accountant, controller (the second person who approves what the administrator creates) or admin. (default "controller") |
Provisioned
| Field | Type | Required | Description |
|---|---|---|---|
organization |
TenantOut |
yes | The new organization, active. |
entities |
array of ProvisionedEntity |
yes | Its entities, with their charts. |
users |
array of ProvisionedUser |
yes | The people invited, the administrator first. Google e-mails each of them the link that sets their password (password_email_sent), except people of the deployment's Google Workspace domains, who sign in with Google. |
replayed |
boolean | True when this is the stored answer of an earlier request with the same Idempotency-Key: nothing was created now. (default false) |
ProvisionedEntity
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | yes | The entity's code. |
name |
string | yes | Legal name. |
country |
string | yes | ISO 3166-1 alpha-2 country code. |
functional_currency |
string | yes | ISO 4217 code of the books' currency. |
chart |
string | yes | The chart template its accounts were copied from. |
role_accounts |
integer | yes | Number of accounts with a role: every role the services need is covered. |
fiscal_years |
array of integer | yes | The fiscal years whose periods were opened. |
vat_method |
string or null | yes | The VAT method saved for the entity; null when the country's default applies. |
ProvisionedUser
| Field | Type | Required | Description |
|---|---|---|---|
id |
string (uuid) | yes | Id (uuid) of the user. |
email |
string | yes | The sign-in address, lower case. |
scopes |
array of string | yes | The scopes the user holds. |
status |
string | yes | invited until the person has signed in. |
password_email_sent |
boolean | True when Google e-mailed the person the link that sets their password (false when password sign-in is not configured, or for an address of a Google Workspace domain, whose people sign in with Google). (default false) |
ServiceHealth
| Field | Type | Required | Description |
|---|---|---|---|
generated_at |
string (date-time) | yes | When the figures were read from Cloud Monitoring (ISO 8601, UTC). The endpoint answers from a cache of at most 60 seconds. |
status |
"degraded" | "ok" | "unavailable" |
yes | ok; degraded when some part could not be read (see problems); unavailable when nothing could (monitoring not configured, or no permission). |
services |
array of ServiceOut |
yes | Both services, ERP first. |
uptime_checks |
array of UptimeCheckOut |
yes | The synthetic checks. |
problems |
array of Problem |
yes | Parts that could not be read; empty when ok. |
ServiceOut
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The Cloud Run service: erp-api or datavnext-api. |
name |
string | yes | The service's name for people. |
slos |
array of SloOut |
yes | Its objectives, availability first. |
latency_p95_ms_24h |
map of number or null | yes | The 95th percentile of the application's response time over the last 24 hours, in milliseconds, by request class: read, report (statements, aging, analytics, FX exposure), write, heavy (PDFs, exports, imports, VAT returns, period close; no objective). Null for a class without requests. |
deploy |
DeployOut or null |
yes | The deploy state; null when Cloud Run could not be read (see problems). |
SloOut
One service level objective of a service, over its rolling window.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The objective's id in Cloud Monitoring, for example availability. |
name |
string | yes | What it promises, in words. |
goal |
number | yes | The internal target as a fraction of good events, for example 0.999. The customer-facing commitment is lower on purpose (docs/legal/sla-draft.md). |
window_days |
integer | yes | The rolling window the compliance is measured over. |
compliance |
number or null | yes | The fraction of good events in the window so far; null without data. |
budget_remaining |
number or null | yes | The share of the error budget still unspent: 1 untouched, 0 spent, negative when the objective is missed; null without data. |
status |
"at_risk" | "breached" | "no_data" | "ok" |
yes | ok; at_risk when less than a quarter of the budget is left; breached when the budget is spent; no_data when the window holds no valid events yet. |
StatusChange
| Field | Type | Required | Description |
|---|---|---|---|
reason |
string | yes | Why the organization is suspended or reactivated, 3 to 500 characters; kept in the organization's audit trail and in the platform's security events. (min length 3; max length 500) |
TenantOut
An organization as the platform sees it: its lifecycle, never its books.
| Field | Type | Required | Description |
|---|---|---|---|
org_id |
string (uuid) | yes | Id (uuid) of the organization. |
name |
string | yes | The organization's name. |
status |
"active" | "suspended" |
yes | active, or suspended: the organization can read but not change anything until it is reactivated. |
created_at |
string (date-time) | yes | When the organization was created (ISO 8601, UTC). |
created_by |
object | yes | Who created it, as an actor object (type, id, name): a platform key (platform:<prefix>) or the command line. |
status_changed_at |
string (date-time) or null | yes | When it was last suspended or reactivated; null if never. |
status_reason |
string or null | yes | Why it was last suspended or reactivated; null if never. |
kind |
"customer" | "demo" |
customer: its books are kept for the statutory period and never deleted. demo: made to be looked at. An operator deletes either in the portal (POST /v1/platform/organizations/{org_id}/delete, ADR-0042). (default "customer") |
|
copy_of |
string (uuid) or null | For a test copy (always a demo), the organization it was made from (ADR-0039); null for every other organization. |
|
remove_requested_at |
string (date-time) or null | When its deletion was asked for (POST /v1/platform/organizations/{org_id}/delete, or the removal of a test copy): nobody reaches it from then on, and an operator can undo it until delete_after (ADR-0042). Null otherwise. |
|
delete_after |
string (date-time) or null | When the owner's job may delete it for good: an hour after remove_requested_at. It runs every hour and at every deploy, so the organization is gone within the hour after this. Until then an operator can undo the deletion (POST /v1/platform/organizations/{org_id}/undo-delete). Null when no deletion was asked for. |
UptimeCheckOut
A synthetic probe from outside Google's network, every five minutes.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | The check's name, for example data-vNext erp. |
url |
string or null | yes | What it fetches. |
status |
"down" | "no_data" | "up" |
yes | up when at least 80% of the probes of the last 15 minutes passed (one region failing is a blip, as in the alert); down otherwise; no_data. |
success_ratio_15m |
number or null | yes | Passed probes in the last 15 minutes. |
success_ratio_24h |
number or null | yes | Passed probes in the last 24 hours. |