Understand Vexless

Interpretations

What an origin means in the ledger, and who said so.

Nothing is inferred

An origin is a document. Which accounts it hits, how its VAT is split, which party carries the receivable: that is an interpretation, and it is authored. By your software, when it knows its own bookkeeping. By an agent. By an accountant. Vexless does not author one. It does not read the lines, apply a rule, or remember what happened to the last bill from the same supplier. It checks that what the author said is a valid entry, posts it, and records who said it.

The interpretation is written and posted in one act. There is no proposal stage, because nothing proposes. The newest version is ACCEPTED; the one it replaced becomes SUPERSEDED; the entry is replaced as a new version.

Postings

An interpretation is a list of postings. The same shape on every origin type, with the origin's counterparty filled in where a party is needed and none was given.

accountNumberstringrequiredFrom the company's chart. Unknown or archived accounts are refused.
amountintegerrequiredBook currency, minor units, signed; debit positive. The postings of one origin sum to zero.
partyIdstringRequired on open-item accounts; defaults to the origin's counterparty
taxCodestringRecords which tax code this posting is reported under
dimensionIdsstring[]At most one dimension per category
descriptionstring
"postings": [
  { "accountNumber": "1700", "amount":  125500 },                              # receivable; party defaults to the buyer
  { "accountNumber": "3010", "amount": -100000, "taxCode": "FI_SALE_25_5" },   # revenue
  { "accountNumber": "2939", "amount":  -25500, "taxCode": "FI_SALE_25_5" } ]  # VAT payable

With the origin

Send postings in the same request as the document and the origin posts in that call. MANUAL requires them; the other three types take them optionally.

Later, on a waiting origin

An origin sent without postings waits at RECEIVED. Anyone with origins:write on the company may supply its first interpretation: your software once it knows, an agent, an accountant.

POST /v1/companies/{companyId}/origins/{originId}/interpretations
X-Actor: agent:coder@2.1
{ "postings": [ { "accountNumber": "6500", "amount": 8000, "taxCode": "FI_PURCHASE_25_5" },
                { "accountNumber": "1763", "amount": 2040, "taxCode": "FI_PURCHASE_25_5" },
                { "accountNumber": "2880", "amount": -10040 } ],
  "reason": "Software subscription" }
{ "id": "org_…", "status": "POSTED", "currentInterpretationId": "int_…",
  "entries": [ { "number": 217, "version": 1, "actor": "agent:coder@2.1", … } ],
  "interpretations": [ { "id": "int_…", "version": 1, "actor": "agent:coder@2.1", "status": "ACCEPTED", "postings": [ … ], "reason": "Software subscription", "createdAt": "…" } ], … }

The response is the origin with explain on. origin.posted fires.

Corrections

Sending postings to an origin that is already posted is a correction, and it needs review:write. A new interpretation version is written, the entry is replaced as a new version under the same number, allocations on the replaced postings are released and their settlements reopened, and origin.posted fires again with the higher version.

POST /v1/companies/{companyId}/origins/{originId}/interpretations
X-Actor: user:tilitoimisto.anna
{ "postings": [ … ], "reason": "Rent, not software" }

An entry dated on or before the company's lock date cannot be replaced (PERIOD_LOCKED). Corrections after lock are a reversal plus a new origin in the open period.

Who decided

Every interpretation carries the actor from the X-Actor header of the request that wrote it, and every entry carries the same actor. Ask for the history with ?explain=true:

GET /v1/companies/{companyId}/origins/{originId}?explain=true
{ …, "currentInterpretationId": "int_02…",
  "interpretations": [
    { "id": "int_01…", "version": 1, "actor": "agent:coder@2.1",         "status": "SUPERSEDED", "postings": [ … ], "reason": "Software subscription" },
    { "id": "int_02…", "version": 2, "actor": "user:tilitoimisto.anna", "status": "ACCEPTED",   "postings": [ … ], "reason": "Rent, not software" } ] }

Enough to tell a user who put a bill where it is, and to show an accountant what the software said before they changed it. When a model eventually authors interpretations, it is an actor like any other, model:…@version, and the chain already carries it.

What Vexless refuses

This is the whole of its judgement. It never substitutes a value of its own.

ENTRY_MUST_BALANCE422The postings do not sum to zero
POSTING_PARTY_REQUIRED422A posting on an open-item account has no party and the origin has no counterparty
ACCOUNT_NOT_FOUND · ACCOUNT_ARCHIVED422
TAX_CODE_NOT_FOUND422
DIMENSION_REQUIRED · DIMENSION_DUPLICATE_CATEGORY422A category required on the account is missing, or a posting carries two dimensions of one category
AMOUNT_NOT_INTEGER400Money is integer minor units
PERIOD_LOCKED · FISCAL_YEAR_CLOSED422The entry to replace is final
ORIGIN_INVALID_STATE422The origin is rejected or reversed
SCOPE_MISSING403A correction without review:write

VAT

The author computes VAT and states it as a posting on the VAT account, tagged with the tax code. Vexless does not compute it and does not check it against the lines; the tax code on a posting records which return line the amount is reported under. The company's tax codes say which VAT account belongs to each code:

GET /v1/tax-codes?jurisdiction=FI
→ { "items": [ { "code": "FI_SALE_25_5", "kind": "SALE", "rateBps": 2550, "returnLine": "301", "vatAccountNumber": "2939", … },
              { "code": "FI_PURCHASE_25_5", "kind": "PURCHASE", "rateBps": 2550, "returnLine": "307", "vatAccountNumber": "1763", … }, … ] }

On a purchase invoice the seller's VAT amounts are authoritative: post what is printed.

Party defaults are hints

An accountant may set defaultAccountNumber and defaultTaxCode on a supplier. They are read back on the party for whoever writes the next interpretation. Vexless never applies them, and nothing an author does teaches Vexless anything. See Parties.

Rejecting

A RECEIVED origin that is not a document, a duplicate the deduplication could not see or a photo of nothing, is rejected by a reviewer with review:write. Terminal; origin.rejected carries the reason.

POST /v1/companies/{companyId}/origins/{originId}/reject
{ "reason": "Not an invoice" }