StabisoDocs

API reference

Approval policy

The decision table that says who may post, approve or release what.

Operation Summary Scope Idempotency-Key
GET /v1/approval-policy Get the approval policy read
PUT /v1/approval-policy Replace the approval policy admin
DELETE /v1/approval-policy Reset the approval policy admin
GET /v1/approval-policy/versions List policy versions read

GET /v1/approval-policy

Get the approval policy

The approval policy in force: a decision table over actor type, action, amount and source whose first matching rule decides allow, require_approval or deny.

source says whether it is the default table or the organization's own; version is 0 until the first change. The response carries an ETag header (e.g. "policy-3"): send it as If-Match with PUT or DELETE to change the policy only if nobody else did in the meantime. actions lists every action the services ask about, agents_always_denied those no table may allow agents.

Required scope read (admin implies every scope)
Response header ETag names the current version
Operation id get_policy_v1_approval_policy_get

Example request

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

Responses

Status Meaning Body
200 OK ApprovalPolicyOut, ETag header
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)

{
  "source": "default",
  "version": 1,
  "updated_at": "2026-09-21T09:30:00Z",
  "updated_by": {},
  "comment": "string",
  "actions": [
    "string"
  ],
  "agents_always_denied": [
    "string"
  ],
  "table": {
    "published_at": "2026-09-21",
    "updated_at": "2026-09-21",
    "sources": [
      {
        "authority": null,
        "url": null,
        "reference": null,
        "verified_at": null,
        "kind": null
      }
    ],
    "id": "string",
    "name": "string",
    "description": "string",
    "domain": "string",
    "hit_policy": "first",
    "inputs": [
      {
        "name": null,
        "type": null,
        "description": null,
        "values": null,
        "required": null,
        "default": null
      }
    ],
    "outputs": [
      {
        "name": null,
        "description": null
      }
    ],
    "rules": [
      {
        "id": null,
        "when": null,
        "then": null,
        "reference": null,
        "note": null
      }
    ],
    "limitations": [
      "string"
    ]
  },
  "pending_change": {
    "approval_id": "string",
    "requested_by": {},
    "requested_at": "2026-09-21T09:30:00Z",
    "comment": "string",
    "source": "default",
    "relaxed": [
      "string"
    ],
    "relaxed_count": 1
  }
}

PUT /v1/approval-policy

Replace the approval policy

Replace the organization's policy with a decision table. Only a human with the admin scope may do this (403 otherwise).

The table is validated (422 with the list of problems): it must declare the four inputs actor_type, action, amount and source (optionally actor_name, a system process's name or else the actor id, never required and tested only with a literal or in) and a decision output, list every action, and deny agents every action but post, whatever the rules say. Each change is a new version and an audit event. Send the ETag you read in If-Match: when the policy has moved on, the answer is 412 and nothing changes. The response carries the new ETag.

A change that relaxes any decision (deny to require_approval or allow, or require_approval to allow, for any actor type, action, amount or source) does not apply yet: it becomes an approval request (change_approval_policy) that a second person with the admin scope approves under Approvals, never the one who asked and never an agent. The response keeps the policy in force and shows the request in pending_change, with what it relaxes. One such change waits at a time (409 approval_policy_change_waiting), until it is decided or the person who asked withdraws it (POST /v1/approvals/{approval_id}/withdraw; an administrator whose colleagues are not administrators has nobody to approve it). A change that only tightens applies at once, and so does any change in an organization with fewer than two active people, unless another user was disabled or lost the admin scope in the last 30 days (audited with what it relaxes).

Required scope admin (admin implies every scope)
Idempotency-Key not used
ETag / If-Match If-Match optional: a changed version answers 412
Response header ETag names the current version
Operation id put_policy_v1_approval_policy_put

Parameters

In Name Type Required Description
header If-Match string or null The ETag of the version you read ("policy-3", from GET /v1/approval-policy). If the policy has changed since, the call answers 412 and changes nothing. * always matches; omitted, the change is made without the check. (max length 200)

Request body

application/json (required): PolicyInput

Field Type Required Description
table PolicyTable yes A decision table with inputs actor_type, action, amount and source and a decision output (allow, require_approval or deny); first hit wins. Start from GET /v1/approval-policy.
comment string or null Why the policy changes, up to 1000 characters; kept with the version and in the audit event. (max length 1000)

Example request

curl -X PUT "$ERP_URL/v1/approval-policy" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H 'If-Match: "1"' \
  -H "Content-Type: application/json" \
  -d '{
  "comment": "Human postings above 250000 need approval",
  "table": {
    "description": "Agents may only post; their manual and large postings are reviewed. Humans may post directly, but a posting above 250000 needs a second person.",
    "domain": "erp.policy",
    "hit_policy": "first",
    "id": "org.approval-policy",
    "inputs": [
      {
        "name": "actor_type",
        "type": "enum",
        "values": [
          "human",
          "agent",
          "system"
        ]
      },
      {
        "name": "action",
        "type": "enum",
        "values": [
          "post",
          "approve",
          "close_period",
          "reopen_period",
          "manage_accounts",
          "manage_keys",
          "release_payments",
          "finalize_vat_return",
          "activate_party",
          "change_bank_details",
          "change_party_details"
        ]
      },
      {
        "description": "Total debits of the entry in functional currency.",
        "name": "amount",
        "type": "number"
      },
      {
        "description": "manual, ar, ap, fa, bank, ...",
        "name": "source",
        "type": "string"
      }
    ],
    "name": "Demo AG approval policy",
    "outputs": [
      {
        "description": "allow | require_approval | deny",
        "name": "decision"
      },
      {
        "name": "reason"
      }
    ],
    "rules": [
      {
        "id": "agents_only_post",
        "then": {
          "decision": "deny",
          "reason": "Agents may only post."
        },
        "when": {
          "action": {
            "not_in": [
              "post"
            ]
          },
          "actor_type": "agent"
        }
      },
      {
        "id": "agent_manual_journals",
        "then": {
          "decision": "require_approval",
          "reason": "Agent journals are reviewed."
        },
        "when": {
          "actor_type": "agent",
          "source": "manual"
        }
      },
      {
        "id": "agent_large_postings",
        "then": {
          "decision": "require_approval",
          "reason": "Agent postings above 5000."
        },
        "when": {
          "actor_type": "agent",
          "amount": {
            "gt": 5000
          }
        }
      },
      {
        "id": "agent_routine_postings",
        "then": {
          "decision": "allow",
          "reason": "Routine subledger posting."
        },
        "when": {
          "actor_type": "agent"
        }
      },
      {
        "id": "system_allowed",
        "then": {
          "decision": "allow",
          "reason": "System processes run under their own controls."
        },
        "when": {
          "actor_type": "system"
        }
      },
      {
        "id": "new_vendors_need_a_second_person",
        "then": {
          "decision": "require_approval",
          "reason": "A new vendor needs a second person."
        },
        "when": {
          "action": "activate_party",
          "actor_type": "human",
          "source": "vendor"
        }
      },
      {
        "id": "human_large_postings",
        "then": {
          "decision": "require_approval",
          "reason": "Postings above 250000."
        },
        "when": {
          "action": "post",
          "actor_type": "human",
          "amount": {
            "gt": 250000
          }
        }
      },
      {
        "id": "humans_allowed",
        "then": {
          "decision": "allow",
          "reason": "Human with the required scope."
        },
        "when": {
          "actor_type": "human"
        }
      }
    ],
    "sources": [
      {
        "authority": "Demo AG",
        "url": "https://example.com/policies/approval"
      }
    ]
  }
}'

Responses

Status Meaning Body
200 OK ApprovalPolicyOut, ETag header
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
412 If-Match names a version that is no longer current (precondition_failed). 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)

{
  "source": "default",
  "version": 1,
  "updated_at": "2026-09-21T09:30:00Z",
  "updated_by": {},
  "comment": "string",
  "actions": [
    "string"
  ],
  "agents_always_denied": [
    "string"
  ],
  "table": {
    "published_at": "2026-09-21",
    "updated_at": "2026-09-21",
    "sources": [
      {
        "authority": null,
        "url": null,
        "reference": null,
        "verified_at": null,
        "kind": null
      }
    ],
    "id": "string",
    "name": "string",
    "description": "string",
    "domain": "string",
    "hit_policy": "first",
    "inputs": [
      {
        "name": null,
        "type": null,
        "description": null,
        "values": null,
        "required": null,
        "default": null
      }
    ],
    "outputs": [
      {
        "name": null,
        "description": null
      }
    ],
    "rules": [
      {
        "id": null,
        "when": null,
        "then": null,
        "reference": null,
        "note": null
      }
    ],
    "limitations": [
      "string"
    ]
  },
  "pending_change": {
    "approval_id": "string",
    "requested_by": {},
    "requested_at": "2026-09-21T09:30:00Z",
    "comment": "string",
    "source": "default",
    "relaxed": [
      "string"
    ],
    "relaxed_count": 1
  }
}

DELETE /v1/approval-policy

Reset the approval policy

Return to the default policy. Only a human with the admin scope may do this (403 otherwise). It is recorded as a new version, so the history keeps the table that was replaced. Send the ETag you read in If-Match; a changed policy answers 412. Returns the default policy with its new ETag. If the default is looser than the organization's table anywhere, the return waits for a second administrator like any change that relaxes the policy (pending_change; see PUT).

Required scope admin (admin implies every scope)
Idempotency-Key not used
ETag / If-Match If-Match optional: a changed version answers 412
Response header ETag names the current version
Operation id reset_policy_v1_approval_policy_delete

Parameters

In Name Type Required Description
header If-Match string or null The ETag of the version you read ("policy-3", from GET /v1/approval-policy). If the policy has changed since, the call answers 412 and changes nothing. * always matches; omitted, the change is made without the check. (max length 200)

Example request

curl -X DELETE "$ERP_URL/v1/approval-policy" \
  -H "Authorization: Bearer $ERP_KEY" \
  -H 'If-Match: "1"'

Responses

Status Meaning Body
200 OK ApprovalPolicyOut, ETag header
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
412 If-Match names a version that is no longer current (precondition_failed). 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)

{
  "source": "default",
  "version": 1,
  "updated_at": "2026-09-21T09:30:00Z",
  "updated_by": {},
  "comment": "string",
  "actions": [
    "string"
  ],
  "agents_always_denied": [
    "string"
  ],
  "table": {
    "published_at": "2026-09-21",
    "updated_at": "2026-09-21",
    "sources": [
      {
        "authority": null,
        "url": null,
        "reference": null,
        "verified_at": null,
        "kind": null
      }
    ],
    "id": "string",
    "name": "string",
    "description": "string",
    "domain": "string",
    "hit_policy": "first",
    "inputs": [
      {
        "name": null,
        "type": null,
        "description": null,
        "values": null,
        "required": null,
        "default": null
      }
    ],
    "outputs": [
      {
        "name": null,
        "description": null
      }
    ],
    "rules": [
      {
        "id": null,
        "when": null,
        "then": null,
        "reference": null,
        "note": null
      }
    ],
    "limitations": [
      "string"
    ]
  },
  "pending_change": {
    "approval_id": "string",
    "requested_by": {},
    "requested_at": "2026-09-21T09:30:00Z",
    "comment": "string",
    "source": "default",
    "relaxed": [
      "string"
    ],
    "relaxed_count": 1
  }
}

GET /v1/approval-policy/versions

List policy versions

Every change to the organization's policy, newest first, with the table of each.

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

Example request

curl "$ERP_URL/v1/approval-policy/versions" \
  -H "Authorization: Bearer $ERP_KEY"

Responses

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

[
  {
    "version": 1,
    "source": "default",
    "comment": "string",
    "created_by": {},
    "created_at": "2026-09-21T09:30:00Z",
    "table": {
      "published_at": null,
      "updated_at": null,
      "sources": [
        null
      ],
      "id": "string",
      "name": "string",
      "description": "string",
      "domain": "string",
      "hit_policy": "first",
      "inputs": [
        null
      ],
      "outputs": [
        null
      ],
      "rules": [
        null
      ],
      "limitations": [
        null
      ]
    }
  }
]

Schemas

ApprovalPolicyOut

Field Type Required Description
source "default" | "organization" yes default, or the organization's own table.
version integer yes 0 until the organization first changes its policy.
updated_at string (date-time) or null yes When the current version was made (ISO 8601, UTC); null at version 0.
updated_by object or null yes The actor that made it, as an actor object; null at version 0.
comment string or null yes The comment given with the current version; null if none.
actions array of string yes Every action the services ask the policy about.
agents_always_denied array of string yes Actions no organization policy can allow agents.
table PolicyTable yes The decision table in force.
pending_change PendingPolicyChange or null A change that relaxes the policy and waits for a second administrator; the table above stays in force until it is approved. Null if none.

PendingPolicyChange

Field Type Required Description
approval_id string yes The approval request a second administrator decides, or the person who asked withdraws (POST /v1/approvals/{approval_id}/withdraw).
requested_by object yes Who asked for it, as an actor object.
requested_at string (date-time) yes When it was asked for (ISO 8601, UTC).
comment string or null yes The comment given with the change; null if none.
source "default" | "organization" yes default (a return to the default table) or organization.
relaxed array of string yes What it relaxes, at most 20, e.g. human change_posting_limit: require_approval -> allow.
relaxed_count integer yes How many decisions it relaxes in all.

PolicyInput

Field Type Required Description
table PolicyTable yes A decision table with inputs actor_type, action, amount and source and a decision output (allow, require_approval or deny); first hit wins. Start from GET /v1/approval-policy.
comment string or null Why the policy changes, up to 1000 characters; kept with the version and in the audit event. (max length 1000)

PolicyTable

Field Type Required Description
published_at string (date) or null When this record entered the dataset.
updated_at string (date) or null When its content last changed after publication.
sources array of Source yes Where the rules come from (at least one), e.g. the company's own policy document. (min items 1)
id string yes Identifier of the table, e.g. org.approval-policy.
name string yes Human-readable name.
description string yes What the table decides, in a sentence or two.
domain string yes e.g. 'tax.indirect', 'standards.us_gaap'.
hit_policy "collect" | "first" first: the first rule whose conditions all match decides (use this for the approval policy). collect: every matching rule is returned. (default "first")
inputs array of PolicyTableInput yes The inputs rules can test. An approval policy declares exactly actor_type, action, amount and source, as in GET /v1/approval-policy.
outputs array of PolicyTableOutput yes What a rule yields. An approval policy declares decision and reason.
rules array of PolicyTableRule yes The rules, tried in order. A rule's when maps an input to a literal or an operator map (in, not_in, ne, gt, gte, lt, lte); an input left out matches anything. (min items 1)
limitations array of string Known gaps of the table, as sentences; may be empty.

PolicyTableInput

Field Type Required Description
name string yes Input name; for an approval policy one of actor_type, action, amount, source.
type "boolean" | "enum" | "number" | "string" yes string, boolean, number or enum; an approval policy needs enum for actor_type and action, number for amount, string for source.
description string or null What the input means; optional.
values array of string or null Allowed values for enum inputs.
required boolean Whether callers must supply the input; default true. (default true)
default decimal string or null Value used when an optional input is not supplied.

PolicyTableOutput

Field Type Required Description
name string yes Output name; an approval policy needs decision (allow, require_approval or deny) and usually reason.
description string or null What the output means; optional.

PolicyTableRule

Field Type Required Description
id string yes Unique id of the rule within the table; returned with each decision as rule_id.
when object Conditions on the inputs, e.g. {"actor_type": "human", "amount": {"gt": 250000}}. Every condition must hold; an empty object always matches.
then object yes The outputs when the rule matches, e.g. {"decision": "allow", "reason": "..."}; decision must be allow, require_approval or deny.
reference string or null Where the rule comes from, e.g. a policy clause; optional.
note string or null A remark on the rule; optional.

PolicyVersionOut

Field Type Required Description
version integer yes Version number, from 1.
source "default" | "organization" yes organization, or default for a reset to the default policy.
comment string or null yes The comment given with this version; null if none.
created_by object yes Who made the version, as an actor object.
created_at string (date-time) yes When it was made (ISO 8601, UTC).
table PolicyTable or null yes null: the default policy.

Source

Field Type Required Description
authority string yes Publisher, e.g. 'European Commission', 'IRS'.
url string (uri) yes Address of the source, an http or https URL. (min length 1)
reference string or null Legal or paragraph reference, e.g. 'UStG §12', 'IAS 16.31'.
verified_at string (date) or null Date a maintainer last checked the value against the source.
kind "official" | "secondary" 'official' = publisher of the law or data; 'secondary' = professional summary used where the official page could not be checked. (default "official")
Stabiso, the Enterprise System of Record · generated from the code and its guides · release 6f03a5e846b3