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.
| accountNumber | stringrequired | From the company's chart. Unknown or archived accounts are refused. |
| amount | integerrequired | Book currency, minor units, signed; debit positive. The postings of one origin sum to zero. |
| partyId | string | Required on open-item accounts; defaults to the origin's counterparty |
| taxCode | string | Records which tax code this posting is reported under |
| dimensionIds | string[] | At most one dimension per category |
| description | string |
"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_BALANCE | 422 | The postings do not sum to zero |
| POSTING_PARTY_REQUIRED | 422 | A posting on an open-item account has no party and the origin has no counterparty |
| ACCOUNT_NOT_FOUND · ACCOUNT_ARCHIVED | 422 | |
| TAX_CODE_NOT_FOUND | 422 | |
| DIMENSION_REQUIRED · DIMENSION_DUPLICATE_CATEGORY | 422 | A category required on the account is missing, or a posting carries two dimensions of one category |
| AMOUNT_NOT_INTEGER | 400 | Money is integer minor units |
| PERIOD_LOCKED · FISCAL_YEAR_CLOSED | 422 | The entry to replace is final |
| ORIGIN_INVALID_STATE | 422 | The origin is rejected or reversed |
| SCOPE_MISSING | 403 | A 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" }