StabisoDocs

API reference

Plans

Budgets and rolling forecasts: plans with versions that a person approves, lines by account and period, and the comparison with what was posted. Never posted to the ledger.

Operation Summary Scope Idempotency-Key
GET /v1/plan-versions/{version_id} Get a plan version with its lines read
POST /v1/plan-versions/{version_id}/approve Approve a plan version approve
GET /v1/plan-versions/{version_id}/comparison Compare a plan version with what was posted read
PUT /v1/plan-versions/{version_id}/lines Set the lines of a draft plan version draft
POST /v1/plan-versions/{version_id}/send-back Send a submitted plan version back to draft draft
POST /v1/plan-versions/{version_id}/submit Submit a plan version for approval draft
GET /v1/plans List plans read
POST /v1/plans Create a plan draft required
GET /v1/plans/{plan_id} Get a plan read
PATCH /v1/plans/{plan_id} Rename a plan draft
POST /v1/plans/{plan_id}/roll Roll a forecast forward from actuals draft required
POST /v1/plans/{plan_id}/versions Start a new version of a plan draft required

GET /v1/plan-versions/{version_id}

Get a plan version with its lines

A version, its status and totals, and every line by account and period. 404 when it does not exist.

Required scope read (admin implies every scope)
Operation id get_plan_version_v1_plan_versions__version_id__get

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).

Example request

curl "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK VersionDetail
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

POST /v1/plan-versions/{version_id}/approve

Approve a plan version

Make a submitted version the plan's current one: reports compare with it from now on, and the version that was current is superseded (it stays, with when). A person's decision: the policy action approve_plan denies agents (403, naming the rule), and by default whoever prepared the version (created it, set its lines, submitted it) does not approve it (403, rule plan_submitter_does_not_approve; a one-person organization allows approve_own_plan). 409 unless it is submitted, when a newer version is current, or version_changed when its lines are not the submitted ones.

Required scope approve (admin implies every scope)
Idempotency-Key not used
Operation id approve_plan_version_v1_plan_versions__version_id__approve_post

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).
query lines_digest string or null The lines_digest the approver looked at: when given, the approval is refused (409 version_changed) unless the lines are still exactly those. (pattern ^[0-9a-f]{64}$)

Example request

curl -X POST "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607/approve" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK VersionOut
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

GET /v1/plan-versions/{version_id}/comparison

Compare a plan version with what was posted

Plan against actual for each period of the version (every regular period of a budget's year; a forecast's periods from its first line to its last): plan, actual, the variance (actual less plan), the variance in percent of the plan and whether it is good news, per account or report line, with revenue, expenses and net income added up. The actuals are the profit and loss of the posted entries by period, closing entries left out and the entries of a year's adjustment periods counted in its last regular period; an open period's actuals are not final (periods[].status). Every row ends with the outlook of each fiscal year: the actuals of its closed periods plus the plan of the others. With against_version, the variance is against that version instead (forecast against budget). Computed when asked; nothing is stored. Read-only. 422 when against_version is this version or another entity's.

Required scope read (admin implies every scope)
Operation id compare_plan_version_v1_plan_versions__version_id__comparison_get

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).
query by "account" | "report_line" Rows by account or by report_line of the profit and loss. (default "account")
query against_version string (uuid) or null Also compare with another version of a plan of the same entity: a budget's current version (current_version_id of GET /v1/plans/{plan_id}) or an earlier forecast. Each cell then carries its amount (against) and the variance is this version's less that one's.

Example request

curl "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607/comparison" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK Comparison
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)

{
  "plan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "version_status": "approved",
  "entity": "string",
  "currency": "string",
  "by": "account",
  "against": "actuals",
  "against_version_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "periods": [
    {
      "period": "string",
      "start_date": "2026-09-21",
      "end_date": "2026-09-21",
      "status": "string"
    }
  ],
  "rows": [
    {
      "key": "string",
      "name": "string",
      "type": "expense",
      "cells": [
        null
      ],
      "total": {
        "period": null,
        "plan": null,
        "actual": null,
        "against": null,
        "variance": null,
        "variance_pct": null,
        "favourable": null
      },
      "outlook": [
        null
      ]
    }
  ],
  "revenue": {
    "key": "string",
    "name": "string",
    "type": "expense",
    "cells": [
      {
        "period": null,
        "plan": null,
        "actual": null,
        "against": null,
        "variance": null,
        "variance_pct": null,
        "favourable": null
      }
    ],
    "total": {
      "period": "string",
      "plan": "string",
      "actual": "string",
      "against": null,
      "variance": "string",
      "variance_pct": null,
      "favourable": null
    },
    "outlook": [
      {
        "fiscal_year": null,
        "actual": null,
        "plan_rest": null,
        "outlook": null,
        "plan": null
      }
    ]
  },
  "expenses": {
    "key": "string",
    "name": "string",
    "type": "expense",
    "cells": [
      {
        "period": null,
        "plan": null,
        "actual": null,
        "against": null,
        "variance": null,
        "variance_pct": null,
        "favourable": null
      }
    ],
    "total": {
      "period": "string",
      "plan": "string",
      "actual": "string",
      "against": null,
      "variance": "string",
      "variance_pct": null,
      "favourable": null
    },
    "outlook": [
      {
        "fiscal_year": null,
        "actual": null,
        "plan_rest": null,
        "outlook": null,
        "plan": null
      }
    ]
  },
  "net": {
    "key": "string",
    "name": "string",
    "type": "expense",
    "cells": [
      {
        "period": null,
        "plan": null,
        "actual": null,
        "against": null,
        "variance": null,
        "variance_pct": null,
        "favourable": null
      }
    ],
    "total": {
      "period": "string",
      "plan": "string",
      "actual": "string",
      "against": null,
      "variance": "string",
      "variance_pct": null,
      "favourable": null
    },
    "outlook": [
      {
        "fiscal_year": null,
        "actual": null,
        "plan_rest": null,
        "outlook": null,
        "plan": null
      }
    ]
  }
}

PUT /v1/plan-versions/{version_id}/lines

Set the lines of a draft plan version

Replace every line of a draft: one amount per revenue or expense account and regular period (YYYY-NN) of the plan's entity, of the budget's year for a budget, in the functional currency and the account's natural direction. Repeating the request sets the same lines. 409 unless the version is a draft; 422 for an unknown or balance sheet account, an unknown or adjustment period, a period outside a budget's year, a repeated account and period, or more decimals than the currency has.

Required scope draft (admin implies every scope)
Idempotency-Key not used
Operation id set_plan_lines_v1_plan_versions__version_id__lines_put

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).

Request body

application/json (required): LinesInput

Field Type Required Description
lines array of PlanLineInput yes Every line of the draft: they replace the lines it has. One amount per account and period; an empty list clears the draft. (max items 20000)

Example request

curl -X PUT "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607/lines" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "lines": [
    {
      "account": "3400",
      "amount": "42000.00",
      "period": "2027-01"
    },
    {
      "account": "5000",
      "amount": "18500.00",
      "period": "2027-01"
    },
    {
      "account": "6000",
      "amount": "3200.00",
      "period": "2027-01"
    }
  ]
}'

Responses

Status Meaning Body
200 OK VersionDetail
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

POST /v1/plan-versions/{version_id}/send-back

Send a submitted plan version back to draft

Return a submitted version to draft, with the reason kept in the audit trail, so its lines can change. 409 unless it is submitted.

Required scope draft (admin implies every scope)
Idempotency-Key not used
Operation id send_back_plan_version_v1_plan_versions__version_id__send_back_post

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).

Request body

application/json (required): SendBackInput

Field Type Required Description
reason string yes Why the version goes back to draft. (min length 3; max length 1000)

Example request

curl -X POST "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607/send-back" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reason": "Marketing figures for Q2 are missing"
}'

Responses

Status Meaning Body
200 OK VersionOut
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

POST /v1/plan-versions/{version_id}/submit

Submit a plan version for approval

Fix a draft's lines for approval: they are digested (lines_digest) and change only after the version is sent back. Open to anyone who may draft, an agent too; approving is a person's decision. 409 unless it is a draft, 422 when it has no lines.

Required scope draft (admin implies every scope)
Idempotency-Key not used
Operation id submit_plan_version_v1_plan_versions__version_id__submit_post

Parameters

In Name Type Required Description
path version_id string (uuid) yes Id of the plan version (GET /v1/plans/{plan_id}, version_list).

Example request

curl -X POST "$ERP_URL/v1/plan-versions/6c1d2e3f-4051-4b62-9c73-8d94ea5fb607/submit" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK VersionOut
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

GET /v1/plans

List plans

Budgets and forecasts, the newest first, each with its current (approved) version and how many versions it has. Read-only.

Required scope read (admin implies every scope)
Operation id list_plans_v1_plans_get

Parameters

In Name Type Required Description
query entity string or null Only this entity's plans (code or id).
query kind "budget" | "forecast" or null Only budget or forecast.
query limit integer How many plans, the newest first. (min 1; max 1000; default 100)

Example request

curl "$ERP_URL/v1/plans" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK array of PlanOut
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)

[
  {
    "created_at": "2026-10-01T08:00:00Z",
    "created_by": {
      "id": "user:alice",
      "name": "Alice",
      "type": "human"
    },
    "currency": "CHF",
    "current_version_id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
    "entity": "CH01",
    "fiscal_year": 2027,
    "id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
    "kind": "budget",
    "name": "Budget 2027",
    "versions": 2
  }
]

POST /v1/plans

Create a plan

Create a budget (one fiscal year, whose periods must exist) or a forecast (a horizon of 3 to 24 periods, 12 by default) for an entity, with an empty draft as version 1. A plan is never posted to the ledger: it is read next to it (GET /v1/plan-versions/{version_id}/ comparison). 409 when the entity already has a plan of that name. Needs an Idempotency-Key.

Required scope draft (admin implies every scope)
Idempotency-Key required
Operation id create_plan_v1_plans_post

Request body

application/json (required): PlanInput

Field Type Required Description
entity string yes Code (or id) of the entity the plan is for.
name string yes What the plan is called, unique within the entity, e.g. Budget 2027. (min length 1; max length 200)
kind "budget" | "forecast" yes budget (the figures of one fiscal year, fixed once approved) or forecast (a number of periods ahead, rolled forward as periods close).
fiscal_year integer or null The fiscal year of a budget (required for a budget, not allowed for a forecast). Its periods must exist (POST /v1/entities/{entity}/fiscal-years). (min 1900; max 2999)
horizon integer or null How many periods a forecast looks ahead, 3 to 24 (default 12); not allowed for a budget. (min 3; max 24)

Example request

curl -X POST "$ERP_URL/v1/plans" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "entity": "CH01",
  "fiscal_year": 2027,
  "kind": "budget",
  "name": "Budget 2027"
}'

Responses

Status Meaning Body
201 Created PlanDetail
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)

{
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "currency": "CHF",
  "current_version_id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "entity": "CH01",
  "fiscal_year": 2027,
  "id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "kind": "budget",
  "name": "Budget 2027",
  "versions": 2
}

GET /v1/plans/{plan_id}

Get a plan

A plan and all its versions, the newest first (without their lines: GET /v1/plan-versions/{version_id}). 404 when it does not exist.

Required scope read (admin implies every scope)
Operation id get_plan_v1_plans__plan_id__get

Parameters

In Name Type Required Description
path plan_id string (uuid) yes Id of the plan (GET /v1/plans).

Example request

curl "$ERP_URL/v1/plans/5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

Status Meaning Body
200 OK PlanDetail
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)

{
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "currency": "CHF",
  "current_version_id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "entity": "CH01",
  "fiscal_year": 2027,
  "id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "kind": "budget",
  "name": "Budget 2027",
  "versions": 2
}

PATCH /v1/plans/{plan_id}

Rename a plan

Change a plan's name (the only thing about a plan that changes; its kind, year and horizon are fixed). 409 when the entity has another plan of that name.

Required scope draft (admin implies every scope)
Idempotency-Key not used
Operation id rename_plan_v1_plans__plan_id__patch

Parameters

In Name Type Required Description
path plan_id string (uuid) yes Id of the plan (GET /v1/plans).

Request body

application/json (required): PlanRename

Field Type Required Description
name string yes The plan's new name. (min length 1; max length 200)

Example request

curl -X PATCH "$ERP_URL/v1/plans/5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Budget 2027 (board)"
}'

Responses

Status Meaning Body
200 OK PlanOut
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)

{
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "currency": "CHF",
  "current_version_id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "entity": "CH01",
  "fiscal_year": 2027,
  "id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "kind": "budget",
  "name": "Budget 2027",
  "versions": 2
}

POST /v1/plans/{plan_id}/roll

Roll a forecast forward from actuals

A new draft of a forecast, rolled from its current version (else its latest): the closed periods of the fiscal year it is in become actuals (read from the ledger now and kept as lines with method actual), the open periods the version planned are carried over, and the periods new to the horizon (the plan's horizon periods from the first open one) are filled by run_rate or prior_year, per account by the method of its latest line where that is one of them. Periods of the horizon that have no fiscal period yet are reported (horizon_short_by), not invented. The draft is reviewed, submitted and approved like any version; open to anyone who may draft, an agent too. 409 for a budget; 422 when every period is closed. Needs an Idempotency-Key.

Required scope draft (admin implies every scope)
Idempotency-Key required
Operation id roll_plan_v1_plans__plan_id__roll_post

Parameters

In Name Type Required Description
path plan_id string (uuid) yes Id of the plan (GET /v1/plans).

Request body

application/json (required): RollInput

Field Type Required Description
method "prior_year" | "run_rate" How the periods new to the horizon are filled, for an account whose lines in the version rolled from say neither: run_rate (the average of the last periods closed periods) or prior_year (the same period a year earlier, closed or forecast, times 1 + growth_pct / 100). An account whose latest line is run_rate or prior_year keeps its own method. (default "run_rate")
periods integer How many closed periods run_rate averages. (min 1; max 12; default 3)
growth_pct decimal string Growth in percent over the prior year, for prior_year (e.g. 5 for 5 %). (min -100; max 1000; default "0")
note string or null What this version is about; empty: a note saying what was rolled. (max length 2000)
refresh boolean Project again the open periods the version rolled from projected (run_rate, prior_year, schedule), keeping only the lines people entered (manual); false carries every open period over as it was. The rolling forecast every company has rolls with it. (default false)

Example request

curl -X POST "$ERP_URL/v1/plans/5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6/roll" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "method": "run_rate",
  "periods": 3
}'

Responses

Status Meaning Body
201 Created RollResult
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)

{
  "version": {
    "approved_at": "2026-10-02T14:10:00Z",
    "approved_by": {
      "id": "user:bob",
      "name": "Bob",
      "type": "human"
    },
    "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
    "created_at": "2026-10-01T08:00:00Z",
    "created_by": {
      "id": "user:alice",
      "name": "Alice",
      "type": "human"
    },
    "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
    "line_count": 3,
    "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
    "note": "After the September review",
    "number": 2,
    "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
    "status": "approved",
    "submitted_at": "2026-10-02T09:30:00Z",
    "submitted_by": {
      "id": "user:alice",
      "name": "Alice",
      "type": "human"
    },
    "totals": {
      "expenses": "21700.00",
      "net": "20300.00",
      "revenue": "42000.00"
    }
  },
  "rolled_from": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "actuals_through": "string",
  "horizon_start": "string",
  "horizon_end": "string",
  "horizon_short_by": 1,
  "lines": {}
}

POST /v1/plans/{plan_id}/versions

Start a new version of a plan

A new draft of the plan with the next number: empty, or a copy of the lines of one of its versions (copy_from), which is how an approved budget is revised. Versions are never deleted, so every figure that was ever current stays on record. Needs an Idempotency-Key.

Required scope draft (admin implies every scope)
Idempotency-Key required
Operation id create_plan_version_v1_plans__plan_id__versions_post

Parameters

In Name Type Required Description
path plan_id string (uuid) yes Id of the plan (GET /v1/plans).

Request body

application/json (required): VersionInput

Field Type Required Description
copy_from string (uuid) or null A version of the same plan whose lines the new draft starts with; empty to start with no lines.
note string or null What this version is about (kept with it). (max length 2000)

Example request

curl -X POST "$ERP_URL/v1/plans/5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6/versions" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "copy_from": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "note": "After the September review"
}'

Responses

Status Meaning Body
201 Created VersionOut
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)

{
  "approved_at": "2026-10-02T14:10:00Z",
  "approved_by": {
    "id": "user:bob",
    "name": "Bob",
    "type": "human"
  },
  "based_on": "7d2e3f40-5162-4c73-8d84-9ea5fb6c7d18",
  "created_at": "2026-10-01T08:00:00Z",
  "created_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "id": "6c1d2e3f-4051-4b62-9c73-8d94ea5fb607",
  "line_count": 3,
  "lines_digest": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
  "note": "After the September review",
  "number": 2,
  "plan_id": "5b0c1d2e-3f40-4a51-8b62-7c83d94ea5f6",
  "status": "approved",
  "submitted_at": "2026-10-02T09:30:00Z",
  "submitted_by": {
    "id": "user:alice",
    "name": "Alice",
    "type": "human"
  },
  "totals": {
    "expenses": "21700.00",
    "net": "20300.00",
    "revenue": "42000.00"
  }
}

Schemas

Comparison

Field Type Required Description
plan_id string (uuid) yes The plan.
version_id string (uuid) yes The version compared.
version_status "approved" | "draft" | "submitted" | "superseded" yes Its status.
entity string yes Entity code.
currency string yes Functional currency of every amount.
by "account" | "report_line" yes account or report_line.
against "actuals" | "version" actuals, or version when compared with another plan version too. (default "actuals")
against_version_id string (uuid) or null The version compared against (against_version).
periods array of ComparisonPeriod yes The periods compared, in order.
rows array of ComparisonRow yes Revenue first, then expenses, each by key: every account (or report line) with a plan or an actual in the periods.
revenue ComparisonRow yes The revenue rows added up.
expenses ComparisonRow yes The expense rows added up.
net ComparisonRow yes Revenue less expenses (type is revenue: more is good news).

ComparisonCell

Field Type Required Description
period string yes The period as YYYY-NN.
plan string yes The planned amount.
actual string yes What was posted: the period's entries (and those of the year's adjustment periods, counted in its last regular period), closing entries left out, in the natural direction.
against string or null With against_version: that version's amount; empty otherwise.
variance string yes Actual less plan; with against_version, this version's amount less that version's.
variance_pct string or null yes Variance as a percent of the plan (with against_version, of that version's amount), one decimal; empty when that is zero.
favourable boolean or null yes True when the variance is good news (more revenue, fewer expenses); empty when it is zero.

ComparisonPeriod

Field Type Required Description
period string yes The period as YYYY-NN.
start_date string (date) yes First day.
end_date string (date) yes Last day.
status string yes open or closed: an open period's actuals are not final yet.

ComparisonRow

Field Type Required Description
key string yes The account code, or the report line.
name string yes The account's name, or the report line again.
type "expense" | "revenue" yes revenue or expense.
cells array of ComparisonCell yes One per period, in order.
total ComparisonCell yes The periods added up (period is total).
outlook array of YearOutlook yes Per fiscal year of the periods compared: actuals of its closed periods plus the plan of the others.

LinesInput

Field Type Required Description
lines array of PlanLineInput yes Every line of the draft: they replace the lines it has. One amount per account and period; an empty list clears the draft. (max items 20000)

PlanDetail

Field Type Required Description
id string (uuid) yes Plan id.
entity string yes Entity code.
name string yes The plan's name.
kind "budget" | "forecast" yes budget or forecast.
fiscal_year integer or null yes The fiscal year of a budget.
horizon integer or null yes How many periods a forecast looks ahead.
currency string yes The functional currency its amounts are in.
current_version_id string (uuid) or null yes The approved version, the one reports compare with; empty before the first approval.
versions integer yes How many versions it has.
created_by object yes Who made it (actor object).
created_at string (date-time) yes When (UTC).
version_list array of VersionOut yes Every version, the newest first.

PlanInput

Field Type Required Description
entity string yes Code (or id) of the entity the plan is for.
name string yes What the plan is called, unique within the entity, e.g. Budget 2027. (min length 1; max length 200)
kind "budget" | "forecast" yes budget (the figures of one fiscal year, fixed once approved) or forecast (a number of periods ahead, rolled forward as periods close).
fiscal_year integer or null The fiscal year of a budget (required for a budget, not allowed for a forecast). Its periods must exist (POST /v1/entities/{entity}/fiscal-years). (min 1900; max 2999)
horizon integer or null How many periods a forecast looks ahead, 3 to 24 (default 12); not allowed for a budget. (min 3; max 24)

PlanLineInput

Field Type Required Description
account string yes Code of a revenue or expense account. (min length 1; max length 50)
period string yes The regular period as YYYY-NN (fiscal year and period number), as the close checklist names it, e.g. 2027-03. (pattern ^\d{4}-\d{2}$)
amount decimal string yes The planned amount in the entity's functional currency, as a decimal string, in the account's natural direction: revenue as a positive credit, expenses as a positive debit (as the profit and loss shows them). A negative amount is a reduction. At most the currency's minor units (2 decimals for CHF and EUR); at most 16 digits in all.
note string or null A note on the amount. (max length 500)

PlanLineOut

Field Type Required Description
account string yes Account code.
account_name string yes Account name.
account_type "expense" | "revenue" yes revenue or expense.
report_line string yes The account's report line, e.g. pl.revenue.
period string yes The period as YYYY-NN.
amount string yes The planned amount in the functional currency.
method "actual" | "manual" | "prior_year" | "run_rate" | "schedule" yes How the amount was made: manual (entered); a rolled forecast's lines also actual, run_rate, prior_year or schedule.
note string or null yes The note on the amount.

PlanOut

Field Type Required Description
id string (uuid) yes Plan id.
entity string yes Entity code.
name string yes The plan's name.
kind "budget" | "forecast" yes budget or forecast.
fiscal_year integer or null yes The fiscal year of a budget.
horizon integer or null yes How many periods a forecast looks ahead.
currency string yes The functional currency its amounts are in.
current_version_id string (uuid) or null yes The approved version, the one reports compare with; empty before the first approval.
versions integer yes How many versions it has.
created_by object yes Who made it (actor object).
created_at string (date-time) yes When (UTC).

PlanRename

Field Type Required Description
name string yes The plan's new name. (min length 1; max length 200)

PlanTotals

Field Type Required Description
revenue string yes Planned revenue (the lines of revenue accounts).
expenses string yes Planned expenses (the lines of expense accounts).
net string yes Revenue less expenses.

RollInput

Field Type Required Description
method "prior_year" | "run_rate" How the periods new to the horizon are filled, for an account whose lines in the version rolled from say neither: run_rate (the average of the last periods closed periods) or prior_year (the same period a year earlier, closed or forecast, times 1 + growth_pct / 100). An account whose latest line is run_rate or prior_year keeps its own method. (default "run_rate")
periods integer How many closed periods run_rate averages. (min 1; max 12; default 3)
growth_pct decimal string Growth in percent over the prior year, for prior_year (e.g. 5 for 5 %). (min -100; max 1000; default "0")
note string or null What this version is about; empty: a note saying what was rolled. (max length 2000)
refresh boolean Project again the open periods the version rolled from projected (run_rate, prior_year, schedule), keeping only the lines people entered (manual); false carries every open period over as it was. The rolling forecast every company has rolls with it. (default false)

RollResult

Field Type Required Description
version VersionOut yes The new draft (a person reviews and approves it).
rolled_from string (uuid) or null yes The version it was rolled from: the plan's current one, else its latest.
actuals_through string or null yes The last closed period, whose actuals the version holds (YYYY-NN); empty when no period of the year is closed yet.
horizon_start string yes The first open period: where the forecast begins.
horizon_end string yes The last period of the horizon that exists.
horizon_short_by integer yes Periods of the horizon that have no fiscal period yet: open the next fiscal year (POST /v1/entities/{entity}/fiscal-years) and roll again to fill them.
lines map of integer yes The new version's lines by method (actual, carried over ones keep theirs).

SendBackInput

Field Type Required Description
reason string yes Why the version goes back to draft. (min length 3; max length 1000)

VersionDetail

Field Type Required Description
id string (uuid) yes Version id.
plan_id string (uuid) yes The plan.
number integer yes The version's number within its plan, from 1.
status "approved" | "draft" | "submitted" | "superseded" yes draft (its lines change), submitted (fixed, waiting for a person to approve it or send it back), approved (the plan's current version) or superseded (approved before a later version).
based_on string (uuid) or null yes The version it was copied from.
note string or null yes What the version is about.
lines_digest string or null yes SHA-256 of the lines as submitted: what the approval is of. Empty for a draft.
line_count integer yes How many lines it has.
totals PlanTotals yes Its revenue, expenses and their difference.
created_by object yes Who made it (actor object).
created_at string (date-time) yes When (UTC).
submitted_by object or null yes Who submitted it.
submitted_at string (date-time) or null yes When it was submitted (UTC).
approved_by object or null yes Who approved it.
approved_at string (date-time) or null yes When it was approved (UTC).
superseded_at string (date-time) or null yes When a later version was approved in its place (UTC).
lines array of PlanLineOut yes The lines, by account and period.

VersionInput

Field Type Required Description
copy_from string (uuid) or null A version of the same plan whose lines the new draft starts with; empty to start with no lines.
note string or null What this version is about (kept with it). (max length 2000)

VersionOut

Field Type Required Description
id string (uuid) yes Version id.
plan_id string (uuid) yes The plan.
number integer yes The version's number within its plan, from 1.
status "approved" | "draft" | "submitted" | "superseded" yes draft (its lines change), submitted (fixed, waiting for a person to approve it or send it back), approved (the plan's current version) or superseded (approved before a later version).
based_on string (uuid) or null yes The version it was copied from.
note string or null yes What the version is about.
lines_digest string or null yes SHA-256 of the lines as submitted: what the approval is of. Empty for a draft.
line_count integer yes How many lines it has.
totals PlanTotals yes Its revenue, expenses and their difference.
created_by object yes Who made it (actor object).
created_at string (date-time) yes When (UTC).
submitted_by object or null yes Who submitted it.
submitted_at string (date-time) or null yes When it was submitted (UTC).
approved_by object or null yes Who approved it.
approved_at string (date-time) or null yes When it was approved (UTC).
superseded_at string (date-time) or null yes When a later version was approved in its place (UTC).

YearOutlook

Field Type Required Description
fiscal_year integer yes The fiscal year.
actual string yes What was posted in its closed periods (compared ones).
plan_rest string yes The plan of its periods that are not closed.
outlook string yes Actual plus the plan of the rest: where the year lands.
plan string yes The plan of all its compared periods, for reference.
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 20b7dd5b0c4d