Build with Vexless
Accounting & reporting
You never write to the ledger. These are the reads a product needs, and what happens on the accounting side without you.
Open items
Every posting on an open-item account, receivables and payables, with something left to clear. The list your software shows as "unpaid invoices" and "bills to pay".
GET /v1/companies/{companyId}/open-items?direction=receivable|payable&partyId=&dueBefore=&account=&cursor=&limit=
{ "items": [ { "id": "pst_…", "postingId": "pst_…", "originId": "org_…", "originType": "SALES_INVOICE", "entryId": "ent_…",
"accountNumber": "1700", "accountName": "Myyntisaamiset",
"partyId": "pty_…", "partyName": "Ostaja Oy", "partyExternalId": "cust-42", "direction": "receivable",
"amount": { "amount": 125500, "currency": "EUR" }, "remaining": { "amount": 100000, "currency": "EUR" },
"externalReference": "2026-000117", "paymentReference": "RF74000117", "invoiceNumber": 117,
"documentDate": "2026-09-03", "dueDate": "2026-09-17" } ],
"next": null }
Amounts are absolute with a direction. The postingId is what an explicit settlement refers to.
Balances
GET /v1/companies/{companyId}/balances?asOf=2026-08-31&accounts=1910,1700&dimensionId=
{ "asOf": "2026-08-31", "from": null, "currency": "EUR",
"accounts": [ { "accountId": "acc_…", "number": "1700", "name": "Myyntisaamiset", "type": "ASSET", "reportingCode": "SAAMISET", "balance": 351000 },
{ "accountId": "acc_…", "number": "1910", "name": "Pankkitili", "type": "ASSET", "reportingCode": "RAHAT_JA_PANKKISAAMISET", "balance": 1204450 } ] }
Without accounts, every active account is returned, zero included. The balance is cumulative at asOf, signed like postings: debit positive, so assets are positive and liabilities, equity and revenue are negative.
GET /v1/companies/{companyId}/balances?from=2026-08-01&asOf=2026-08-31 # with from, balance is the movement within the range
GET /v1/companies/{companyId}/balances?fiscalYearId=fy_… # shorthand for the year's range
Income statement and balance sheet
GET /v1/companies/{companyId}/reports/income-statement?from=2026-01-01&to=2026-08-31 # or ?fiscalYearId=
{ "from": "2026-01-01", "to": "2026-08-31", "currency": "EUR",
"lines": [ { "code": "LIIKEVAIHTO", "label": "Liikevaihto", "amount": 4820000 },
{ "code": "LIIKETOIMINNAN_MUUT_KULUT", "label": "Liiketoiminnan muut kulut", "amount": -1912300 }, … ],
"result": { "amount": 2907700, "currency": "EUR" } }
GET /v1/companies/{companyId}/reports/balance-sheet?asOf=2026-08-31
{ "asOf": "2026-08-31", "currency": "EUR",
"assets": [ { "code": "SAAMISET", "label": "Saamiset", "amount": 351000 }, { "code": "RAHAT_JA_PANKKISAAMISET", … } ],
"equityAndLiabilities": [ { "code": "TILIKAUDEN_VOITTO", "label": "Tilikauden voitto", "amount": 2907700 }, … ],
"totals": { "assets": { "amount": …, "currency": "EUR" }, "equityAndLiabilities": { "amount": …, "currency": "EUR" } } }
Report lines are the statutory small-company layouts, grouped by each account's reportingCode. Revenue is positive and expenses negative on the income statement. The balance sheet's current-year result line includes the not-yet-closed profit, and both totals agree at any date.
Entries
GET /v1/companies/{companyId}/entries?from=&to=&account=&cursor=&limit=
GET /v1/companies/{companyId}/entries/{entryId}
An entry: id, revision, number, date, description, originId, interpretationId, actor, reversesEntryId, postings[]. Numbers are sequential per fiscal year. While its period is open, a correction rewrites the entry in place under the same number and bumps revision; once the period is locked the entry is immutable and a correction is a reversal plus a new entry. The history of what was decided lives on the origin’s interpretations, each with its own actor and reason — ask for it with ?explain=true. Every entry names its origin and the interpretation and actor behind it, which is the audit chain in one object.
Dimensions
Your own labels for reporting: Location, Project, Team. A line or posting carries at most one dimension per category. They never change how anything is posted, and balances?dimensionId= filters by them.
POST /v1/companies/{companyId}/dimensions
{ "category": "Location", "name": "Helsinki" } # creates the category on first use; origins:write
{ "id": "dim_…", "categoryId": "dcat_…" }
GET /v1/companies/{companyId}/dimensions # { items: [{ id, name, requiredOnAccounts, dimensions: [{ id, name }] }] }
A category with requiredOnAccounts makes postings on those account ranges refuse to post without one of its dimensions (DIMENSION_REQUIRED).
Manual entries
For the rare case your software knows exactly what to book and there is no document: accruals, a bank fee you learned about elsewhere, a correction the accountant asked for. A MANUAL origin is nothing but postings and a description. It must balance, and postings on open-item accounts need a party.
POST /v1/companies/{companyId}/origins
{ "type": "MANUAL", "description": "Bank fee", "documentDate": "2026-09-05",
"postings": [ { "accountNumber": "6900", "amount": 500 }, { "accountNumber": "1910", "amount": -500 } ] }
Chart of accounts and tax codes
Both come with the company. Read them to validate what you send and to label what you show; see Companies.
GET /v1/companies/{companyId}/accounts # { items: [{ id, number, name, type, openItems, status, reportingCode }] }
GET /v1/tax-codes?jurisdiction=FI # { items: [{ code, name, kind, rateBps, validFrom, validTo, returnLine, vatAccountNumber }] }
The accounting side
These happen in Vexless without you. An accountant does them in a Vexless control surface, or through the same API with accountant scopes. You observe the results.
- Interpretation. Origins you sent without postings are interpreted or rejected. You see
origin.received, thenorigin.postedororigin.rejected. It is one call; there is no proposal to approve. - Corrections. An entry is replaced by a new version with the same number.
origin.postedfires again with a higherversion. - Lock. The lock date moves forward, usually monthly.
company.lockedcarriesthrough. Entries on or before that date are final. An origin dated inside the locked range posts on the first open date; its document date is kept. - Year-end close. Revenue and expense accounts are zeroed into the result account on the last day of the year and the result moves to retained earnings on the first day of the next. The year locks.
fiscal_year.closed. Period figures for that year still come frombalances?fiscalYearId=and the income statement, while the balance sheet carries the result in equity. - Residuals. Settlements that cannot reach zero are closed with a write-off, fee or FX difference booked as a
SYSTEMorigin.
VAT returns and balance-level reconciliation are part of the design and not exposed in this version.
Events you will see
| company.locked | through | Entries on or before this date are final |
| fiscal_year.closed | fiscalYearId, startDate, endDate, result, closingOriginId, openingOriginId | Result moved to equity; the year is locked |
| vat_return.filed | Defined; not emitted in this version |