Vexless API
Bookkeeping as a backend. Your software sends the documents it already has. Vexless keeps the books, numbers invoices, matches the money, sends payments, and tells you what is paid.
Overview
Vexless is a headless system of record for bookkeeping, built for software companies whose customers run a business. Your customers use your software. Their books are kept in Vexless without them, or you, doing any accounting.
You never choose accounts, compute VAT or post to a ledger. You send an origin: a sales invoice you issued, a bill your user wants paid, a bank transaction. Vexless interprets it, posts it, matches money against it, and reports back through statuses and webhooks.
Anything a model is unsure about is handled by an accountant in a separate Vexless control surface. You do not build for it. What it produces reaches you as the same statuses and events.
First jurisdiction: Finland. The OpenAPI document is the contract; every response is typed there and enforced by the server. The endpoint reference at the end of this page is generated from it.
https://api.vexless.io/v1 # base URL of the service; reserved, not live yet
https://docs.vexless.io/ # this reference
https://docs.vexless.io/openapi.json # OpenAPI 3.1, fully typed responses; also /openapi.yaml
https://docs.vexless.io/llms.txt # for agents; /llms-full.txt has the whole reference as text
Getting started
Six requests take one customer from nothing to a paid invoice. All of them work on a sandbox company with no real bank involved.
1. Get an API key
Vexless issues your organisation, a partner, an API key. It starts with vx_ and is shown once. Every request carries it as a bearer token.
2. Create the customer's company
POST /v1/companies
Authorization: Bearer vx_…
X-Actor: agent:onboarding@1.0
Idempotency-Key: company-asiakas-oy
Content-Type: application/json
{ "name": "Asiakas Oy", "businessIdentifier": "1234567-8", "jurisdiction": "FI", "sandbox": true }
201
{ "id": "cmp_06g6…", "name": "Asiakas Oy", "currency": "EUR", "sandbox": true, "lockDate": null, "createdAt": "2026-09-03T09:12:03.120Z",
"connections": [ { "kind": "PARTNER_API", … }, { "kind": "EINVOICE", … }, { "kind": "AUTHORITY", … }, { "kind": "BANK", "provider": "sandbox", "status": "ACTIVE", … } ],
"externalAccounts": [ { "kind": "BANK", "externalId": "FI0000000000000000", "accountNumber": "1910", … } ],
"fiscalYears": [ { "id": "fy_…", "startDate": "2026-01-01", "endDate": "2026-12-31", "closedAt": null, … } ] }
The company arrives with the Finnish chart of accounts, tax codes, an e-invoice and an authority connection, and, because it is a sandbox, a simulated bank account. Your partner is granted the standard scopes on it. GET /v1/companies lists every company you hold a grant on.
3. Subscribe to events
POST /v1/companies/cmp_06g6…/webhooks
{ "url": "https://your.app/vexless", "events": ["origin.posted", "origin.settled", "origin.needs_review", "payment.executed", "payment.rejected"] }
201
{ "id": "whk_…", "secret": "…" } # keep the secret; it signs every delivery
4. Issue a sales invoice
POST /v1/companies/cmp_06g6…/origins
Idempotency-Key: inv-2026-000117
{
"type": "SALES_INVOICE",
"externalReference": "2026-000117",
"buyer": { "businessId": "7654321-1", "name": "Ostaja Oy", "externalId": "cust-42" },
"dueDate": "2026-09-17",
"lines": [ { "description": "Consulting, August 2026", "net": 100000, "hint": "consulting" } ]
}
201
{ "id": "org_06g6…", "type": "SALES_INVOICE", "status": "POSTED",
"invoiceNumber": 1, "paymentReference": "RF74000001",
"total": { "amount": 125500, "currency": "EUR" },
"settlement": { "state": "unpaid", "paid": 0, "remaining": 125500, "paidAt": null, "paidBy": [], "currency": "EUR" },
"delivery": { "channel": "PDF_EMAIL", "status": "PENDING_PROVIDER" },
"currentInterpretationId": "int_…", "confidence": 1,
"entries": [ { "number": 1, "version": 1, "date": "2026-09-03", "postings": [
{ "accountNumber": "1700", "accountName": "Myyntisaamiset", "amount": 125500, "partyId": "pty_…", "remaining": 125500, … },
{ "accountNumber": "3010", "accountName": "Palvelumyynti", "amount": -100000, "taxCode": "FI_SALE_25_5", … },
{ "accountNumber": "2939", "accountName": "Arvonlisäverovelka", "amount": -25500, "taxCode": "FI_SALE_25_5", … } ] } ], … }
You sent net lines and a business id. Vexless resolved the buyer, applied 25.5 % VAT, assigned the invoice number and RF reference, and posted the entry.
5. Show what is open
GET /v1/companies/cmp_06g6…/open-items?direction=receivable
200
{ "items": [ { "originId": "org_06g6…", "partyName": "Ostaja Oy", "partyExternalId": "cust-42", "direction": "receivable",
"amount": { "amount": 125500, "currency": "EUR" }, "remaining": { "amount": 125500, "currency": "EUR" },
"externalReference": "2026-000117", "paymentReference": "RF74000001", "dueDate": "2026-09-17", … } ],
"next": null }
6. The customer pays
In production the bank connection delivers the transaction. In the sandbox, and for any bank feed you operate yourself, you send it:
POST /v1/companies/cmp_06g6…/origins
{ "type": "BANK_TRANSACTION", "externalId": "bank-tx-8812", "iban": "FI0000000000000000",
"amount": 125500, "bookingDate": "2026-09-15", "reference": "RF74000001",
"counterparty": { "name": "OSTAJA OY", "iban": "FI2112345600000785" } }
201 → status POSTED; postings 1910 +125500, 1700 −125500, allocated to the invoice by reference
Your webhook receives origin.settled for the invoice with your external reference. Its status is now SETTLED. That is the whole loop.
Authentication and access
Every request carries Authorization: Bearer vx_…. The key identifies your partner. What you may do on a company is decided by the grant your partner holds on it.
Creating a company grants the creating partner these scopes: origins:write, ledger:read, settlements:write, payments:write, connections:manage, grants:manage. With grants:manage you can give another partner, an accounting office for instance, or yourself, further scopes:
POST /v1/companies/{companyId}/grants
{ "granteeKind": "OFFICE", "granteeId": "ptn_…", "scopes": ["review:write", "accounts:manage", "close:manage"] }
| Scope | | Allows |
| origins:write | vertical | Send origins and attachments, define dimensions, name parties, reverse origins you delivered |
| ledger:read | vertical | Read everything; manage webhooks |
| settlements:write | vertical | Allocate explicitly |
| payments:write | vertical | Create and cancel payments; sandbox execution |
| connections:manage | vertical | Start bank connections |
| grants:manage | owner | Create and revoke grants |
| review:write | accountant | Accept, reject and override interpretations; settlement proposals; close settlements with a residual; reverse any origin |
| accounts:manage | accountant | Create accounts, change party defaults, change auto-accept policies |
| close:manage | accountant | Lock periods, close fiscal years |
A request without a valid key answers 401 UNAUTHENTICATED. A request on a company your partner has no grant on answers 404 COMPANY_NOT_FOUND. A missing scope answers 403 SCOPE_MISSING.
Money, dates, ids, paging
| Money | object | { "amount": 125500, "currency": "EUR" }. Amounts are integers in minor units, never floats. In request bodies a bare amount field such as net or amount is minor units in the origin's currency. |
| Sign | | Ledger postings and balances: debit positive, credit negative, so assets are positive and liabilities, equity and revenue are negative. Bank transactions: positive is money in. Open items and reports present absolute amounts with a direction. |
| Dates | string | YYYY-MM-DD for business dates. RFC 3339 with milliseconds and Z for timestamps: 2026-09-03T10:16:37.767Z. |
| Ids | string | Prefixed and time-sortable: cmp_, org_, ent_, pst_, pty_, pay_, stl_, int_, att_, fy_, whk_. |
| Lists | | Every list endpoint returns { "items": [...], "next": "cursor" | null }. Pass ?cursor= to continue; limit is 1 to 200, default 50. Small bounded lists such as accounts return next: null. The cursor is the id of the last item. |
| Currency | | Books are kept in the company currency (EUR). An origin in another currency needs currency and fxRate (book units per origin unit). The original amount is kept on the postings. |
| Future dates | | Sales invoices, purchase invoices and bank transactions dated more than 60 days ahead are refused with DATE_TOO_FAR_AHEAD. Manual entries are exempt. Fiscal years are created as calendar years when an entry first needs one. |
Errors
Every refused request answers with the rule it broke, a message for a person, and a JSON pointer into the request when one applies. Extra keys carry context.
{ "rule": "ORIGIN_DUPLICATE", "message": "An origin with externalReference 2026-000117 already exists",
"path": "/externalReference", "existingId": "org_06g6…" }
| Status | Rule | |
| 400 | VALIDATION_FAILED | Body or query did not match the schema. issues[] lists every problem. |
| 400 | ACTOR_REQUIRED · ACTOR_INVALID · IDEMPOTENCY_KEY_REQUIRED · AMOUNT_NOT_INTEGER | |
| 400 | PARTY_UNRESOLVED | No identifier to resolve or create a party from |
| 401 | UNAUTHENTICATED | |
| 403 | SCOPE_MISSING · COMPANY_NOT_SANDBOX | |
| 404 | COMPANY_NOT_FOUND · ORIGIN_NOT_FOUND · PARTY_NOT_FOUND · EXTERNAL_ACCOUNT_NOT_FOUND · POSTING_NOT_FOUND · ATTACHMENT_NOT_FOUND · FISCAL_YEAR_NOT_FOUND · NOT_FOUND | |
| 409 | ORIGIN_DUPLICATE | Same content or same external reference already received; existingId names it |
| 409 | IDEMPOTENCY_CONFLICT · IDEMPOTENCY_IN_PROGRESS | differs says whether the path or the body changed |
| 413 | ATTACHMENT_TOO_LARGE | 10 MB decoded |
| 422 | ENTRY_MUST_BALANCE · POSTING_PARTY_REQUIRED · DIMENSION_REQUIRED · DIMENSION_DUPLICATE_CATEGORY | Ledger invariants, mostly reachable through MANUAL origins |
| 422 | ACCOUNT_NOT_FOUND · ACCOUNT_ARCHIVED · TAX_CODE_NOT_FOUND | |
| 422 | DATE_TOO_FAR_AHEAD | See future dates above |
| 422 | PERIOD_LOCKED · FISCAL_YEAR_CLOSED | An existing entry on or before the lock date cannot change |
| 422 | ORIGIN_INVALID_STATE · INTERPRETATION_INVALID_STATE · PAYMENT_INVALID_STATE | The object is not in a state that allows this action; status says which |
| 422 | ORIGIN_NOT_REVERSIBLE | Only POSTED or SETTLED origins reverse, and never an invoice with payments allocated |
| 422 | PAYMENT_NO_PAYEE_IBAN | Your company has not supplied an IBAN for the seller; add one with a party PATCH |
| 422 | SETTLEMENT_INVALID · SETTLEMENT_ACCOUNT_MISMATCH · ALLOCATION_EXCEEDS_REMAINING · POSTING_NOT_OPEN_ITEM | |
| 422 | FISCAL_YEAR_NOT_READY · LOCK_BLOCKED | Origins in the year still await review; the lock only moves forward |
Companies
One per customer of yours. Created with the jurisdiction's chart of accounts, tax codes, an e-invoice connection, an authority connection and a calendar fiscal year.
POST /v1/companies
{ "name": "Asiakas Oy", "businessIdentifier": "1234567-8", "jurisdiction": "FI",
"sandbox": true, # optional; enables the simulated bank and sandbox endpoints
"fiscalYearStart": "2026-01-01" } # optional; defaults to 1 January of the current year
GET /v1/companies?cursor=&limit= # every company your partner holds a grant on
GET /v1/companies/{companyId} # connections, externalAccounts, fiscalYears, lockDate
GET /v1/companies/{companyId}/accounts # { items: [{ id, number, name, type, openItems, status, reportingCode }] }
GET /v1/companies/{companyId}/fiscal-years
GET /v1/tax-codes?jurisdiction=FI # { items: [{ code, name, kind, rateBps, validFrom, validTo, returnLine }] }
Every account carries a reportingCode, the statutory report line it rolls up to. The reports group by it.
Auto-accept policy
Per origin type, the company decides what posts without an accountant: a minimum confidence, an optional maximum amount, and whether the counterparty must already be known. Defaults: 0.9 for sales invoices, purchase invoices and bank transactions; purchase invoices additionally require a known supplier. This is why a supplier's first bill always goes to review, however good your hint. An accountant with accounts:manage can change it:
GET /v1/companies/{companyId}/policies/auto-accept
PUT /v1/companies/{companyId}/policies/auto-accept/PURCHASE_INVOICE
{ "minConfidence": 0.8, "maxAmount": 500000, "knownPartyOnly": false }
Bank connections
Sandbox companies come with an active simulated bank whose account is FI0000000000000000, mapped to ledger account 1910. For a real bank:
POST /v1/companies/{companyId}/connections
{ "kind": "BANK", "provider": "op" }
201 → { "id": "con_…", "status": "NEEDS_AUTH", "consentUrl": "https://…" } # send your user there
Real bank providers are not connected in this version; the connection stays in NEEDS_AUTH. Bank transactions can always be sent through the origins endpoint.
Dimensions
Your own labels for reporting: Location, Project, Team. A line carries at most one dimension per category. They never change how anything is posted.
POST /v1/companies/{companyId}/dimensions
{ "category": "Location", "name": "Helsinki" } # creates the category on first use
201 → { "id": "dim_…", "categoryId": "dcat_…" }
GET /v1/companies/{companyId}/dimensions # { items: [{ id, name, requiredOnAccounts, dimensions: [{ id, name }] }] }
Origins
The document behind every entry. One endpoint, four types, discriminated by type. The response is always the origin, already interpreted and, when policy allows, posted.
POST /v1/companies/{companyId}/origins # 201, or 409 ORIGIN_DUPLICATE
POST /v1/companies/{companyId}/origins?dryRun=true # 200; runs the whole pipeline and rolls back; response carries "dryRun": true
GET /v1/companies/{companyId}/origins/{originId}?explain=true
GET /v1/companies/{companyId}/origins?type=&status=&partyId=&from=&to=&externalReference=&settlementState=&cursor=&limit=
POST /v1/companies/{companyId}/origins/{originId}/reverse # undo a posted origin you delivered; see below
Naming a party
Wherever a party is expected, send any one identifier. Vexless resolves it or creates the party. Identifiers learned on one company recognise the same legal party on another, which is why matching works from day one. What you get back is always your own view of the party: the name you gave, or the identifier you gave when you gave no name, and only the identifiers your company supplied. Nothing another company knows about the party is shown to you.
| businessId | string | Y-tunnus, e.g. 1234567-8 |
| vatId | string | FI12345678 |
| iban | string | Also the payee account when you pay this party |
| einvoiceAddress | string | Enables e-invoice delivery to this party |
| externalId | string | Your id for the party in this company; returned on every origin, open item and event |
| partyId | string | A pty_ id you received earlier |
| name, country | string | name becomes the name your company sees |
Lines
| description | stringrequired | |
| net | integerrequired | Net in the origin currency. Negative on credit notes. |
| taxCode | string | e.g. FI_SALE_25_5, FI_PURCHASE_14, FI_PURCHASE_EU_SERVICES. Company default when omitted. |
| vat | integer | The VAT amount as printed. Authoritative on purchase invoices and used to infer the tax code. |
| hint | string | Free text such as rent, software, consulting. Mapped to an account when the pack knows it. |
| dimensionIds | string[] | |
| quantity, unit | | Kept for invoice rendering |
SALES_INVOICE
An invoice you issued to your customer's customer. Vexless assigns invoiceNumber and the RF paymentReference. Delivery goes by e-invoice when the buyer has an e-invoice address, otherwise as a PDF.
| buyer | partyrequired | |
| dueDate | daterequired | |
| lines | line[]required | At least one |
| externalReference | string | Your invoice id. Deduplicates and comes back on every event. |
| documentDate | date | Defaults to today; at most 60 days ahead |
| currency, fxRate | | For foreign-currency invoices |
| data | object | Anything you want stored with the document |
A credit note is a sales invoice with negative lines.
PURCHASE_INVOICE
A bill your customer received. In production most arrive through the e-invoice connection. You send the ones you capture yourself.
| seller | partyrequired | Include iban if you will pay it through Vexless |
| documentDate | daterequired | |
| lines | line[]required | Send vat per line as printed |
| externalReference | string | The supplier's invoice number. Also used to recognise the payment later. |
| dueDate | date | |
| total | integer | Gross total as printed; refused if it does not equal lines net plus VAT |
| paymentReference | string | The supplier's RF or Finnish reference |
| currency, fxRate, data | | |
A supplier's first bill always goes to an accountant. The default policy requires a known supplier, and the interpreter's confidence for an unknown one tops out at 0.8 even with a perfect hint. Once one bill from that supplier has been accepted or corrected, its account and tax code are remembered and the next bill posts on its own.
BANK_TRANSACTION
One line of a bank statement. Vexless posts the bank side and decides the counter side by matching against open items, then by the counterparty's default account, otherwise to a suspense account for review.
| externalId | stringrequired | The bank's transaction id; deduplicates |
| iban | string | The company's own account. Sandbox: FI0000000000000000. Alternative: externalAccountId. |
| amount | integerrequired | Signed; positive is money in |
| bookingDate | daterequired | At most 60 days ahead |
| valueDate | date | |
| counterparty | party | Usually name and iban as the bank reports them |
| reference | string | RF or Finnish reference on the payment; the strongest match signal |
| message | string | Free-text message; invoice numbers and supplier invoice numbers in it are used for matching |
| settles | [{ originId, amount? }] | When your software knows which invoices this transaction pays, say so. Allocations are then exact and marked EXPLICIT. |
| currency, fxRate | | |
MANUAL
Explicit postings, for the rare case your software knows exactly what to book. Must balance. Postings on receivables, payables or other open-item accounts need a party.
{ "type": "MANUAL", "description": "Bank fee", "documentDate": "2026-09-05",
"postings": [ { "accountNumber": "6900", "amount": 500 }, { "accountNumber": "1910", "amount": -500 } ] }
Reversing an origin
A bank feed can push the same transaction twice under different ids, or a captured bill can be simply wrong. POST …/origins/{originId}/reverse books a new entry with the postings negated, clears the original's open items against it, and sets the origin to REVERSED. The original entry stays as history and both entries appear on the origin. With origins:write you may reverse origins your own partner delivered; review:write may reverse any. An invoice that already has payments allocated to it cannot be reversed: issue a credit note or have a reviewer correct it.
The origin response
Returned by every origin endpoint, and carried in part by every origin event.
| id, type | | |
| status | enum | RECEIVED → NEEDS_REVIEW | POSTED → SETTLED; REJECTED by an accountant; REVERSED after a reversal |
| parties | [{ role, partyId, name, externalId }] | Roles: BUYER, SELLER, PAYER, PAYEE, ISSUER. name is your company's name for the party. |
| counterpartyId | string | null | |
| documentDate, dueDate | date | |
| currency, total | money | Gross, in the origin currency |
| externalReference | string | null | Yours, or the supplier's, or the bank's |
| paymentReference | string | null | RF reference; assigned on sales invoices |
| invoiceNumber | integer | null | Sales invoices, sequential per company |
| lines, data | | As sent |
| settlement | object | null | { state: unpaid | partial | settled, paid, remaining, paidAt, paidBy[], currency }. paidBy lists the origins that paid it. Null on non-invoice origins. |
| delivery | object | null | Sales invoices: { channel: EINVOICE | PDF_EMAIL, status } |
| currentInterpretationId | string | null | The accepted interpretation behind the current entry; null while in review |
| confidence | number | null | Of the current interpretation, or of the latest proposal while in review |
| attachments | attachment[] | See attachments |
| entries | entry[] | Current ledger entries; after a reversal there are two. Each: id, version, number, date, description, actor, reversesEntryId, postings[]. A posting: accountNumber, accountName, amount, currency, originalAmount, originalCurrency, partyId, taxCode, dimensionIds, remaining (remaining only on open-item accounts). |
| payments | [{ id, status, amount, executeOn, bankTransactionOriginId }] | |
| interpretations | with ?explain=true | Every version: actor (e.g. rule:interpreter@1, user:mikko), confidence, status, notes[], postings[], decidedBy, reason. Enough to tell a user why a bill landed where it did. |
| createdBy, createdAt, updatedAt | | |
An origin can be posted more than once. If an accountant corrects it, its entry gets a new version with the same number, and origin.posted fires again. Handle events idempotently on originId plus version.
Attachments
The PDF, XML or photo behind an origin. Vexless stores it, keeps it for the statutory retention period, and deduplicates by content.
POST /v1/companies/{companyId}/origins/{originId}/attachments
{ "filename": "receipt.jpg", "mediaType": "image/jpeg", "contentBase64": "…" } # at most 10 MB decoded
201 → { "id": "att_…", "originId": "org_…", "filename": "receipt.jpg", "mediaType": "image/jpeg", "size": 48213,
"hash": "sha256…", "retentionUntil": "2032-12-31", "createdBy": "user:mikko", "createdAt": "…" }
GET /v1/companies/{companyId}/origins/{originId}/attachments # { items, next }
GET /v1/companies/{companyId}/origins/{originId}/attachments/{attachmentId} # the bytes, with Content-Type and X-Vexless-Sha256
Sending the same content twice returns the existing attachment. Retention is six years from the end of the document's year.
Parties
GET /v1/companies/{companyId}/parties?externalId=&cursor=&limit=
200
{ "items": [ { "id": "prl_…", "partyId": "pty_…", "name": "Toimittaja Oy", "roles": ["SUPPLIER"], "externalId": "sup-88",
"defaultAccountNumber": "6000", "defaultTaxCode": "FI_PURCHASE_25_5",
"identifiers": [ { "scheme": "BUSINESS_ID", "value": "1111111-1", "source": "DOCUMENT" }, { "scheme": "IBAN", … } ], … } ],
"next": null }
PATCH /v1/companies/{companyId}/parties/{partyId}
{ "name": "Our Supplier", "externalId": "sup-88", "iban": "FI4250001510000023" } # origins:write
{ "defaultAccountNumber": "6000", "defaultTaxCode": "FI_PURCHASE_25_5" } # accounts:manage
The defaults are what the interpreter uses for that supplier's bills. They are learned automatically when an accountant accepts or corrects a bill, and can be set directly with accounts:manage. An IBAN you add here is one Vexless may pay to.
Ledger reads and reports
You never write to the ledger. These are the reads a product needs.
GET /v1/companies/{companyId}/open-items?direction=receivable|payable&partyId=&dueBefore=&account=&cursor=&limit=
→ { items: [{ id, postingId, originId, originType, entryId, accountNumber, partyId, partyName, partyExternalId, direction,
amount, remaining, externalReference, paymentReference, invoiceNumber, documentDate, dueDate }], next }
GET /v1/companies/{companyId}/balances?asOf=2026-08-31&accounts=1910,1700&dimensionId=
→ { asOf, from: null, currency, accounts: [{ accountId, number, name, type, reportingCode, balance }] }
every active account, zero included; balance is cumulative at asOf, signed
GET /v1/companies/{companyId}/balances?from=2026-08-01&asOf=2026-08-31
GET /v1/companies/{companyId}/balances?fiscalYearId=fy_…
→ with from (or a fiscal year), balance is the movement within the range
GET /v1/companies/{companyId}/reports/income-statement?from=&to= # or ?fiscalYearId=
→ { from, to, currency, lines: [{ code, label, amount }], result } revenue positive, expenses negative
GET /v1/companies/{companyId}/reports/balance-sheet?asOf=
→ { asOf, currency, assets: [...], equityAndLiabilities: [...], totals } the current-year result line includes the not-yet-closed profit
GET /v1/companies/{companyId}/entries?from=&to=&account=&cursor=&limit=
GET /v1/companies/{companyId}/entries/{entryId}
GET /v1/companies/{companyId}/entries/{entryId}/versions # { items } — history after corrections
Report lines are the statutory small-company layouts, grouped by each account's reportingCode. Both totals of the balance sheet agree at any date.
Settlements
Most allocation happens automatically. Use these when your software knows the link better than the matcher, for example from a card processor's payout report. The simplest form is settles on the bank transaction itself. The explicit form works on posting ids from open items:
POST /v1/companies/{companyId}/settlements
{ "allocations": [ { "debitPostingId": "pst_invoice…", "creditPostingId": "pst_payment…", "amount": 125500 } ] }
201 → { id, accountNumber, status: OPEN | CLOSED, allocations: [{ debitPostingId, creditPostingId, amount, basis, actor }] }
GET /v1/companies/{companyId}/settlements/{settlementId}
Both postings must be on the same open-item account, one debit and one credit, and the amount cannot exceed either remaining. Overpayments stay as an open credit on the payer. Closing a settlement that does not net to zero, with a write-off, fee or FX difference, is an accountant action.
Payments
Vexless pays purchase invoices. Approval is your workflow: the POST is the approval, and the actor on it is recorded as the approver. Nothing is posted when a payment is sent; the ledger moves when the bank transaction arrives, and the payment links the two.
POST /v1/companies/{companyId}/payments
X-Actor: user:mikko
{ "originId": "org_bill…", "executeOn": "2026-09-30", "amount": 50200 } # amount defaults to remaining; executeOn to the due date
201
{ "id": "pay_…", "originId": "org_bill…", "status": "CREATED",
"amount": { "amount": 50200, "currency": "EUR" }, "executeOn": "2026-09-30",
"payeeIban": "FI4250001510000023", "paymentReference": null, "bankReference": null, "bankTransactionOriginId": null, … }
GET /v1/companies/{companyId}/payments/{paymentId}
POST /v1/companies/{companyId}/payments/{paymentId}/cancel # while CREATED
Status moves CREATED → SENT → ACCEPTED → EXECUTED, or REJECTED with a rejectReason, or CANCELLED. The payee IBAN must be one your company supplied for the seller; without one the call answers PAYMENT_NO_PAYEE_IBAN. A payment on a bill still in review is accepted and held until the bill is posted.
Webhooks and events
POST /v1/companies/{companyId}/webhooks { "url": "https://…", "events": ["origin.settled", …], "secret": "optional, ≥16 chars" }
201 → { "id": "whk_…", "secret": "…" }
GET /v1/companies/{companyId}/webhooks # { items, next }
DELETE /v1/companies/{companyId}/webhooks/{webhookId}
Delivery
Each event is POSTed as JSON to every active webhook subscribed to its type. Delivery is at least once, with retries at 30 seconds, 2 minutes, 10 minutes, 1 hour and then every 6 hours after a non-2xx response or a timeout of 10 seconds. Order is not guaranteed. Answer 2xx quickly and do the work asynchronously.
POST https://your.app/vexless
Content-Type: application/json
X-Vexless-Event: origin.settled
X-Vexless-Event-Id: evt_…
X-Vexless-Signature: t=1757000000,v1=<hex hmac-sha256(secret, t + "." + rawBody)>
{ "id": "evt_…", "type": "origin.settled", "companyId": "cmp_…", "occurredAt": "2026-09-15T09:12:03.120Z",
"originId": "org_…", "externalReference": "2026-000117", "paidAt": "2026-09-15",
"paid": { "amount": 125500, "currency": "EUR" }, "paidBy": ["org_banktx…"] }
Verify by recomputing the HMAC over t, a dot, and the raw body, and comparing with v1 in constant time.
Events
| Type | Payload keys beyond id, type, companyId, occurredAt | |
| origin.received | originId, type, externalReference | A document arrived |
| origin.needs_review | originId, type, externalReference, confidence, notes[] | An accountant will resolve it; nothing needed from you |
| origin.posted | originId, type, externalReference, entryId, version, entryNumber, confidence, actor | Fires again with a higher version on correction |
| origin.settled | originId, type, externalReference, paidAt, paid, paidBy[] | Fully paid or applied |
| origin.rejected | originId, type, externalReference, reason | An accountant decided it is not a document |
| origin.reversed | originId, type, externalReference, reversalEntryId, reversesEntryId, reason | Undone; any invoice it had paid is open again |
| payment.accepted | paymentId, originId, amount, bankReference | The bank took the transfer |
| payment.rejected | paymentId, originId, reason | |
| payment.executed | paymentId, originId, bankTransactionOriginId, amount | Money moved; the bill's origin.settled follows |
| company.locked | through | Entries on or before this date are final; later origins post on the next open date |
| fiscal_year.closed | fiscalYearId, startDate, endDate, result, closingOriginId, openingOriginId | Result moved to equity; the year is locked |
| invoice.delivered · connection.needs_auth · reconciliation.broken · vat_return.filed | | Defined; not emitted in this version |
Sandbox
A company created with "sandbox": true behaves exactly like a live one, with a simulated bank instead of a real one. Use it to build and test the whole loop.
- The company's bank account is
FI0000000000000000 on ledger account 1910. Send incoming payments as BANK_TRANSACTION origins on that IBAN.
- Outgoing payments are simulated on request. Execution books today, or on
executeOn when that date has already passed; pass bookingDate to choose.
POST /v1/companies/{companyId}/sandbox/payments/{paymentId}/execute { "bookingDate": "2026-09-30" } # body optional
POST /v1/companies/{companyId}/sandbox/payments/{paymentId}/reject { "reason": "insufficient funds" }
Sandbox endpoints on a live company answer 403 COMPANY_NOT_SANDBOX.
How an origin is processed
you or a connection
OriginDeduplicated on content and external reference. Parties resolved or created.
vexless
InterpretationPostings proposed with a confidence. Stored before anything happens.
vexless
PolicyWithin the company's policy it posts at once. Otherwise it waits for an accountant.
vexless
EntryNumbered per fiscal year. Postings sum to zero.
vexless → you
Settlement and eventsMoney allocated to invoices. Webhooks fired.
Confidence is mechanical. A sales invoice from net lines is 1.0. An unmapped hint costs 0.1. An unknown supplier costs 0.2, and a bill with neither a supplier default nor a hint costs 0.4 more. A bank transaction matched by reference is 1.0; matched by party and amount 0.85 or lower; parked on suspense 0.3.
Matching a bank transaction, in order: the RF or Finnish reference; an invoice number or the supplier's invoice number in the message; the recognised party with exactly one open item of that amount; the oldest due when several open items share the amount; the recognised party with several items summing to it. Your settles list overrides all of it.
Learning without machine learning: when an accountant accepts or corrects a bill, the chosen account and tax code become that supplier's defaults, so the next bill posts on its own.
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.
- Review. Origins outside the policy are accepted, corrected or rejected. You see
origin.needs_review, then origin.posted or origin.rejected. A correction is applied and accepted in one call; there is no second step.
- Corrections. An entry is replaced by a new version with the same number.
origin.posted fires again.
- Lock. The lock date moves forward, usually monthly.
company.locked. 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. After it, period figures for that year still come from balances?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 system origin.
Endpoint reference
Generated from the OpenAPI document at build time: every operation with its parameters, request body and response schema, then the named schemas the responses refer to.
Companies and access
POST/v1/companies
Create a company · scope any key
Request body
| Field | Type | Description |
| namerequired | string | |
| businessIdentifierrequired | string | |
| jurisdictionrequired | FI | |
| sandbox | boolean | |
| fiscalYearStart | string | |
Response 201 · Company
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies
List the companies your partner holds a grant on · scope any key
Query
| Field | Type | Description |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | CompanySummary[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}
Get a company with its connections, external accounts and fiscal years · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200 · Company
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/grants
Grant scopes on a company to a partner · scope grants:manage
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| granteeKindrequired | VERTICAL | OFFICE | AGENT | OWNER | |
| granteeIdrequired | string | Partner id |
| scopesrequired | origins:write | ledger:read | settlements:write | payments:write | connections:manage | review:write | accounts:manage | close:manage | grants:manage[] | |
Response 201
| Field | Type | Description |
| id | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
DELETE/v1/companies/{companyId}/grants/{grantId}
Revoke a grant · scope grants:manage
Path
| Field | Type | Description |
| companyId | string | |
| grantId | string | |
Response 200
| Field | Type | Description |
| ok | true | |
Errors: 400401403404409413422 as { rule, message, path? }.
Origins and attachments
POST/v1/companies/{companyId}/origins
Send a document: sales invoice, purchase invoice, bank transaction or manual entry · scope origins:write
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| dryRun | boolean | |
Request body
type = SALES_INVOICE
| Field | Type | Description |
| typerequired | SALES_INVOICE | |
| externalReference | string | Your id for the invoice |
| buyerrequired | object | Any one identifier is enough; Vexless resolves or creates the party |
| buyer.partyId | string | |
| buyer.name | string | |
| buyer.businessId | string | Y-tunnus / business identifier |
| buyer.vatId | string | |
| buyer.iban | string | |
| buyer.einvoiceAddress | string | |
| buyer.externalId | string | Your id for this party |
| buyer.country | string | |
| documentDate | string | |
| dueDaterequired | string | |
| currency | string | |
| fxRate | number | Required when currency differs from the company currency |
| linesrequired | object[] | |
| lines[].descriptionrequired | string | |
| lines[].netrequired | integer | Net amount in the origin currency; negative on credit notes |
| lines[].taxCode | string | e.g. FI_SALE_25_5; company default when omitted |
| lines[].vat | integer | Explicit VAT; authoritative on purchase invoices |
| lines[].hint | string | Free-text category hint, e.g. rent |
| lines[].dimensionIds | string[] | |
| lines[].quantity | number | |
| lines[].unit | string | |
| data | object | |
type = PURCHASE_INVOICE
| Field | Type | Description |
| typerequired | PURCHASE_INVOICE | |
| externalReference | string | Supplier's invoice number |
| sellerrequired | object | Any one identifier is enough; Vexless resolves or creates the party |
| seller.partyId | string | |
| seller.name | string | |
| seller.businessId | string | Y-tunnus / business identifier |
| seller.vatId | string | |
| seller.iban | string | |
| seller.einvoiceAddress | string | |
| seller.externalId | string | Your id for this party |
| seller.country | string | |
| documentDaterequired | string | |
| dueDate | string | |
| currency | string | |
| fxRate | number | |
| linesrequired | object[] | |
| lines[].descriptionrequired | string | |
| lines[].netrequired | integer | Net amount in the origin currency; negative on credit notes |
| lines[].taxCode | string | e.g. FI_SALE_25_5; company default when omitted |
| lines[].vat | integer | Explicit VAT; authoritative on purchase invoices |
| lines[].hint | string | Free-text category hint, e.g. rent |
| lines[].dimensionIds | string[] | |
| lines[].quantity | number | |
| lines[].unit | string | |
| total | integer | Gross total as printed; validated against lines |
| paymentReference | string | Supplier's payment reference (RF or Finnish) |
| data | object | |
type = BANK_TRANSACTION
| Field | Type | Description |
| typerequired | BANK_TRANSACTION | |
| externalIdrequired | string | The bank's transaction id |
| externalAccountId | string | |
| iban | string | The company's own account; alternative to externalAccountId |
| amountrequired | integer | Signed; positive = money in |
| bookingDaterequired | string | |
| valueDate | string | |
| counterparty | object | Any one identifier is enough; Vexless resolves or creates the party |
| counterparty.partyId | string | |
| counterparty.name | string | |
| counterparty.businessId | string | Y-tunnus / business identifier |
| counterparty.vatId | string | |
| counterparty.iban | string | |
| counterparty.einvoiceAddress | string | |
| counterparty.externalId | string | Your id for this party |
| counterparty.country | string | |
| reference | string | |
| message | string | |
| currency | string | |
| fxRate | number | |
| settles | object[] | Origins this transaction settles, when your software knows |
| settles[].originIdrequired | string | |
| settles[].amount | integer | Integer minor units (cents) |
type = MANUAL
| Field | Type | Description |
| typerequired | MANUAL | |
| descriptionrequired | string | |
| documentDaterequired | string | |
| externalReference | string | |
| counterparty | object | Any one identifier is enough; Vexless resolves or creates the party |
| counterparty.partyId | string | |
| counterparty.name | string | |
| counterparty.businessId | string | Y-tunnus / business identifier |
| counterparty.vatId | string | |
| counterparty.iban | string | |
| counterparty.einvoiceAddress | string | |
| counterparty.externalId | string | Your id for this party |
| counterparty.country | string | |
| postingsrequired | object[] | |
| postings[].accountNumberrequired | string | |
| postings[].amountrequired | integer | Integer minor units (cents) |
| postings[].partyId | string | |
| postings[].taxCode | string | |
| postings[].dimensionIds | string[] | |
| postings[].description | string | |
| data | object | |
Response 200
| Field | Type | Description |
| id | string | |
| type | string | |
| status | string | RECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED |
| parties | object[] | |
| parties[].role | string | |
| parties[].partyId | string | |
| parties[].name | string | null | |
| parties[].externalId | string | null | |
| counterpartyId | string | null | |
| documentDate | string | ISO date YYYY-MM-DD |
| dueDate | string | null | |
| currency | string | |
| total | Money | |
| externalReference | string | null | |
| paymentReference | string | null | |
| invoiceNumber | integer | null | |
| lines | object[] | |
| data | object | |
| settlement | SettlementState | null | |
| settlement.state | unpaid | partial | settled | |
| settlement.paid | integer | |
| settlement.remaining | integer | |
| settlement.paidAt | string | null | |
| settlement.paidBy | string[] | Origins that paid this one |
| settlement.currency | string | |
| delivery | object | null | |
| delivery.channel | string | |
| delivery.status | string | |
| currentInterpretationId | string | null | |
| confidence | number | null | |
| attachments | Attachment[] | |
| createdBy | string | |
| createdAt | string | RFC 3339 timestamp |
| updatedAt | string | RFC 3339 timestamp |
| entries | Entry[] | |
| payments | object[] | |
| payments[].id | string | |
| payments[].status | string | |
| payments[].amount | Money | |
| payments[].executeOn | string | ISO date YYYY-MM-DD |
| payments[].bankTransactionOriginId | string | null | |
| interpretations | Interpretation[] | Present with ?explain=true |
| dryRun | true | |
Response 201 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/origins
List origins · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| type | SALES_INVOICE | PURCHASE_INVOICE | BANK_TRANSACTION | MANUAL | SYSTEM | PAYROLL | |
| status | RECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED | |
| partyId | string | |
| from | string | |
| to | string | |
| externalReference | string | |
| settlementState | unpaid | partial | settled | |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | Origin[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/origins/{originId}
Get an origin with its entries, settlement and payments · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Query
| Field | Type | Description |
| explain | boolean | |
Response 200 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/origins/{originId}/reverse
Reverse a posted origin with a negated entry · scope origins:write for origins you delivered; review:write for any
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Request body
| Field | Type | Description |
| reason | string | |
Response 200 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/origins/{originId}/attachments
Attach a file to an origin · scope origins:write
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Request body
| Field | Type | Description |
| filenamerequired | string | |
| mediaTyperequired | string | e.g. application/pdf, image/jpeg |
| contentBase64required | string | File content, base64; at most 10 MB decoded |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/origins/{originId}/attachments
List an origin's attachments · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Response 200
| Field | Type | Description |
| items | Attachment[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/origins/{originId}/attachments/{attachmentId}
Download an attachment · scope ledger:read Returns the file bytes with its media type; not JSON.
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
| attachmentId | string | |
Response 200
File bytes with the attachment's media type.
Ledger reads and reports
GET/v1/companies/{companyId}/entries
List current ledger entries · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| from | string | |
| to | string | |
| account | string | |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | Entry[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/entries/{entryId}
Get the current version of an entry · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| entryId | string | |
Response 200 · Entry
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/entries/{entryId}/versions
List every version of an entry · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| entryId | string | |
Response 200
| Field | Type | Description |
| items | Entry[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/balances
Balances per account, cumulative or for a range · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| asOf | string | |
| from | string | With from, balances are movements in [from, asOf] |
| fiscalYearId | string | Shorthand for from and asOf of that year |
| accounts | string | Comma-separated account numbers |
| dimensionId | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/open-items
Open receivables and payables · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| direction | receivable | payable | |
| partyId | string | |
| dueBefore | string | |
| account | string | |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | OpenItem[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/reports/income-statement
Income statement for a range or a fiscal year · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| from | string | |
| to | string | |
| fiscalYearId | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/reports/balance-sheet
Balance sheet at a date · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| asOf | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
Settlements
POST/v1/companies/{companyId}/settlements
Allocate open items explicitly · scope settlements:write
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| allocationsrequired | object[] | |
| allocations[].debitPostingIdrequired | string | |
| allocations[].creditPostingIdrequired | string | |
| allocations[].amountrequired | integer | Integer minor units (cents) |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/settlements/{settlementId}
Get a settlement with its allocations · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| settlementId | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/settlements/{settlementId}/close
Close a settlement by booking the residual · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| settlementId | string | |
Request body
| Field | Type | Description |
| kind | WRITE_OFF | FX_DIFFERENCE | FEE | YEAR_END | VAT_SETTLEMENT | |
| date | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
Payments
POST/v1/companies/{companyId}/payments
Pay a purchase invoice; the call is the approval · scope payments:write
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| originIdrequired | string | |
| amount | integer | Integer minor units (cents) |
| executeOn | string | |
Response 201 · Payment
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/payments/{paymentId}
Get a payment · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| paymentId | string | |
Response 200 · Payment
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/payments/{paymentId}/cancel
Cancel a payment that has not been sent · scope payments:write
Path
| Field | Type | Description |
| companyId | string | |
| paymentId | string | |
Response 200 · Payment
Errors: 400401403404409413422 as { rule, message, path? }.
Parties
GET/v1/companies/{companyId}/parties
List parties as this company knows them · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| externalId | string | |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | Party[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
PATCH/v1/companies/{companyId}/parties/{partyId}
Update your name, external id, IBAN or defaults for a party · scope origins:write; defaults need accounts:manage
Path
| Field | Type | Description |
| companyId | string | |
| partyId | string | |
Request body
| Field | Type | Description |
| name | string | The name this company knows the party by |
| externalId | string | null | |
| defaultAccountNumber | string | null | |
| defaultTaxCode | string | null | |
| iban | string | |
Response 200 · Party
Errors: 400401403404409413422 as { rule, message, path? }.
Chart, tax codes, dimensions, policies
GET/v1/companies/{companyId}/accounts
List the chart of accounts · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200
| Field | Type | Description |
| items | Account[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/accounts
Add an account · scope accounts:manage
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| numberrequired | string | |
| namerequired | string | |
| typerequired | ASSET | LIABILITY | EQUITY | REVENUE | EXPENSE | |
| openItems | boolean | |
| reportingCode | string | |
Response 201
| Field | Type | Description |
| id | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/tax-codes
List tax codes for a jurisdiction · scope any key
Query
| Field | Type | Description |
| jurisdiction | string | |
Response 200
| Field | Type | Description |
| items | TaxCode[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/dimensions
List dimension categories and dimensions · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200
| Field | Type | Description |
| items | DimensionCategory[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/dimensions
Create a dimension, and its category on first use · scope origins:write
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| categoryrequired | string | |
| namerequired | string | |
| requiredOnAccounts | object[] | |
| requiredOnAccounts[].fromrequired | string | |
| requiredOnAccounts[].torequired | string | |
Response 201
| Field | Type | Description |
| id | string | |
| categoryId | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/policies/auto-accept
List auto-accept policies per origin type · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200
| Field | Type | Description |
| items | AutoAcceptPolicy[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
PUT/v1/companies/{companyId}/policies/auto-accept/{originType}
Set the auto-accept policy for an origin type · scope accounts:manage
Path
| Field | Type | Description |
| companyId | string | |
| originType | SALES_INVOICE | PURCHASE_INVOICE | BANK_TRANSACTION | MANUAL | SYSTEM | PAYROLL | |
Request body
| Field | Type | Description |
| minConfidencerequired | number | |
| maxAmount | integer | null | Integer minor units (cents) |
| knownPartyOnlyrequired | boolean | |
Response 200
| Field | Type | Description |
| items | AutoAcceptPolicy[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
Connections
POST/v1/companies/{companyId}/connections
Start a bank connection; returns a consent link · scope connections:manage
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| kindrequired | BANK | |
| providerrequired | string | |
Response 201
| Field | Type | Description |
| id | string | |
| status | string | |
| consentUrl | string | null | |
Errors: 400401403404409413422 as { rule, message, path? }.
Webhooks
POST/v1/companies/{companyId}/webhooks
Subscribe a URL to events · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| urlrequired | string (uri) | |
| eventsrequired | origin.received | origin.needs_review | origin.posted | origin.settled | origin.rejected | origin.reversed | invoice.delivered | payment.accepted | payment.rejected | payment.executed | connection.needs_auth | company.locked | fiscal_year.closed | reconciliation.broken | vat_return.filed[] | |
| secret | string | |
Response 201
| Field | Type | Description |
| id | string | |
| secret | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/webhooks
List webhooks · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200
| Field | Type | Description |
| items | Webhook[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
DELETE/v1/companies/{companyId}/webhooks/{webhookId}
Disable a webhook · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
| webhookId | string | |
Response 200
| Field | Type | Description |
| ok | true | |
Errors: 400401403404409413422 as { rule, message, path? }.
Review (accountant scopes)
POST/v1/companies/{companyId}/origins/{originId}/reject
Reject an origin awaiting review · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Request body
| Field | Type | Description |
| reason | string | |
Response 200 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/origins/{originId}/interpretations
Override the postings of an origin; applied and accepted in one call · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| originId | string | |
Request body
| Field | Type | Description |
| postingsrequired | object[] | |
| postings[].accountNumberrequired | string | |
| postings[].amountrequired | integer | Integer minor units (cents) |
| postings[].partyId | string | |
| postings[].taxCode | string | |
| postings[].dimensionIds | string[] | |
| postings[].description | string | |
| reason | string | |
Response 201 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/interpretations/{interpretationId}/accept
Accept a proposed interpretation · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| interpretationId | string | |
Request body
| Field | Type | Description |
| reason | string | |
Response 200 · Origin
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/interpretations/{interpretationId}/reject
Reject a proposed interpretation · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| interpretationId | string | |
Request body
| Field | Type | Description |
| reason | string | |
Response 200
| Field | Type | Description |
| ok | true | |
Errors: 400401403404409413422 as { rule, message, path? }.
GET/v1/companies/{companyId}/settlement-proposals
List settlement proposals · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Query
| Field | Type | Description |
| status | PROPOSED | ACCEPTED | REJECTED | SUPERSEDED | |
| cursor | string | |
| limit | integer | |
Response 200
| Field | Type | Description |
| items | SettlementProposal[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/settlement-proposals/{proposalId}/accept
Accept a settlement proposal · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| proposalId | string | |
Response 200
| Field | Type | Description |
| ok | true | |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/settlement-proposals/{proposalId}/reject
Reject a settlement proposal · scope review:write
Path
| Field | Type | Description |
| companyId | string | |
| proposalId | string | |
Response 200
| Field | Type | Description |
| ok | true | |
Errors: 400401403404409413422 as { rule, message, path? }.
Close (accountant scopes)
GET/v1/companies/{companyId}/fiscal-years
List fiscal years · scope ledger:read
Path
| Field | Type | Description |
| companyId | string | |
Response 200
| Field | Type | Description |
| items | FiscalYear[] | |
| next | string | null | Cursor for the next page, or null |
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/lock
Move the lock date forward · scope close:manage
Path
| Field | Type | Description |
| companyId | string | |
Request body
| Field | Type | Description |
| throughrequired | string | |
Response 200 · Company
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/fiscal-years/{fiscalYearId}/close
Close a fiscal year: result to equity, year locked · scope close:manage
Path
| Field | Type | Description |
| companyId | string | |
| fiscalYearId | string | |
Errors: 400401403404409413422 as { rule, message, path? }.
Sandbox
POST/v1/companies/{companyId}/sandbox/payments/{paymentId}/execute
Sandbox: the bank accepts and executes a payment · scope payments:write; sandbox companies only
Path
| Field | Type | Description |
| companyId | string | |
| paymentId | string | |
Request body
| Field | Type | Description |
| bookingDate | string | Defaults to today, or to executeOn when that is in the past |
Response 200 · Payment
Errors: 400401403404409413422 as { rule, message, path? }.
POST/v1/companies/{companyId}/sandbox/payments/{paymentId}/reject
Sandbox: the bank rejects a payment · scope payments:write; sandbox companies only
Path
| Field | Type | Description |
| companyId | string | |
| paymentId | string | |
Request body
| Field | Type | Description |
| reason | string | |
Response 200 · Payment
Errors: 400401403404409413422 as { rule, message, path? }.
GET/health
Liveness check · scope none
Response 200
| Field | Type | Description |
| ok | boolean | |
Schemas
Named objects the responses refer to. Every field is present in every response; nullable fields are marked.
Money
| Field | Type | Description |
| amount | integer | Integer minor units |
| currency | string | |
Error
| Field | Type | Description |
| rule | string | |
| message | string | |
| path | string | |
Company
| Field | Type | Description |
| id | string | |
| name | string | |
| businessIdentifier | string | |
| jurisdiction | string | |
| currency | string | |
| lockDate | string | null | |
| sandbox | boolean | |
| createdAt | string | RFC 3339 timestamp |
| connections | object[] | |
| connections[].id | string | |
| connections[].kind | string | |
| connections[].provider | string | |
| connections[].status | string | |
| connections[].consentUrl | string | null | |
| externalAccounts | object[] | |
| externalAccounts[].id | string | |
| externalAccounts[].connectionId | string | |
| externalAccounts[].kind | string | |
| externalAccounts[].externalId | string | |
| externalAccounts[].accountNumber | string | |
| fiscalYears | FiscalYear[] | |
CompanySummary
| Field | Type | Description |
| id | string | |
| name | string | |
| businessIdentifier | string | |
| jurisdiction | string | |
| currency | string | |
| lockDate | string | null | |
| sandbox | boolean | |
| createdAt | string | RFC 3339 timestamp |
FiscalYear
| Field | Type | Description |
| id | string | |
| startDate | string | ISO date YYYY-MM-DD |
| endDate | string | ISO date YYYY-MM-DD |
| closedAt | string | null | |
| closedBy | string | null | |
| resultAmount | integer | null | |
| closingOriginId | string | null | |
| openingOriginId | string | null | |
Account
| Field | Type | Description |
| id | string | |
| number | string | |
| name | string | |
| type | string | |
| openItems | boolean | |
| status | string | |
| reportingCode | string | null | |
| createdAt | string | RFC 3339 timestamp |
TaxCode
| Field | Type | Description |
| id | string | |
| jurisdiction | string | |
| code | string | |
| name | string | |
| kind | string | |
| rateBps | integer | |
| validFrom | string | ISO date YYYY-MM-DD |
| validTo | string | null | |
| returnLine | string | |
| vatAccountNumber | string | |
DimensionCategory
| Field | Type | Description |
| id | string | |
| name | string | |
| requiredOnAccounts | object[] | |
| requiredOnAccounts[].from | string | |
| requiredOnAccounts[].to | string | |
| dimensions | object[] | |
| dimensions[].id | string | |
| dimensions[].name | string | |
AutoAcceptPolicy
| Field | Type | Description |
| originType | string | |
| minConfidence | number | |
| maxAmount | integer | null | |
| knownPartyOnly | boolean | |
Party
| Field | Type | Description |
| id | string | Relationship id; the paging cursor |
| partyId | string | |
| name | string | The name this company knows the party by |
| country | string | null | |
| roles | string[] | |
| externalId | string | null | |
| defaultAccountNumber | string | null | |
| defaultTaxCode | string | null | |
| identifiers | object[] | Only identifiers this company supplied |
| identifiers[].scheme | string | |
| identifiers[].value | string | |
| identifiers[].source | string | |
| createdAt | string | RFC 3339 timestamp |
Origin
| Field | Type | Description |
| id | string | |
| type | string | |
| status | string | RECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED |
| parties | object[] | |
| parties[].role | string | |
| parties[].partyId | string | |
| parties[].name | string | null | |
| parties[].externalId | string | null | |
| counterpartyId | string | null | |
| documentDate | string | ISO date YYYY-MM-DD |
| dueDate | string | null | |
| currency | string | |
| total | Money | |
| externalReference | string | null | |
| paymentReference | string | null | |
| invoiceNumber | integer | null | |
| lines | object[] | |
| data | object | |
| settlement | SettlementState | null | |
| settlement.state | unpaid | partial | settled | |
| settlement.paid | integer | |
| settlement.remaining | integer | |
| settlement.paidAt | string | null | |
| settlement.paidBy | string[] | Origins that paid this one |
| settlement.currency | string | |
| delivery | object | null | |
| delivery.channel | string | |
| delivery.status | string | |
| currentInterpretationId | string | null | |
| confidence | number | null | |
| attachments | Attachment[] | |
| createdBy | string | |
| createdAt | string | RFC 3339 timestamp |
| updatedAt | string | RFC 3339 timestamp |
| entries | Entry[] | |
| payments | object[] | |
| payments[].id | string | |
| payments[].status | string | |
| payments[].amount | Money | |
| payments[].executeOn | string | ISO date YYYY-MM-DD |
| payments[].bankTransactionOriginId | string | null | |
| interpretations | Interpretation[] | Present with ?explain=true |
Entry
| Field | Type | Description |
| id | string | |
| version | integer | |
| number | integer | |
| date | string | ISO date YYYY-MM-DD |
| description | string | |
| originId | string | |
| interpretationId | string | |
| actor | string | |
| isCurrent | boolean | |
| reversesEntryId | string | null | |
| createdAt | string | RFC 3339 timestamp |
| postings | Posting[] | |
Posting
| Field | Type | Description |
| id | string | |
| accountId | string | |
| accountNumber | string | |
| accountName | string | |
| amount | integer | Book currency, signed; debit positive |
| currency | string | |
| originalAmount | integer | null | |
| originalCurrency | string | null | |
| fxRate | number | null | |
| partyId | string | null | |
| taxCode | string | null | |
| dimensionIds | string[] | |
| description | string | null | |
| remaining | integer | null | Open-item accounts only; signed like amount |
Attachment
| Field | Type | Description |
| id | string | |
| originId | string | |
| filename | string | |
| mediaType | string | |
| size | integer | |
| hash | string | |
| retentionUntil | string | null | |
| createdBy | string | |
| createdAt | string | RFC 3339 timestamp |
Interpretation
| Field | Type | Description |
| id | string | |
| version | integer | |
| actor | string | |
| confidence | number | |
| status | string | |
| notes | string[] | |
| postings | object[] | |
| decidedBy | string | null | |
| decidedAt | string | null | |
| reason | string | null | |
| createdAt | string | RFC 3339 timestamp |
SettlementState
| Field | Type | Description |
| state | unpaid | partial | settled | |
| paid | integer | |
| remaining | integer | |
| paidAt | string | null | |
| paidBy | string[] | Origins that paid this one |
| currency | string | |
OpenItem
| Field | Type | Description |
| id | string | Posting id; the paging cursor |
| postingId | string | |
| originId | string | |
| originType | string | |
| entryId | string | |
| accountNumber | string | |
| accountName | string | |
| partyId | string | null | |
| partyName | string | null | |
| partyExternalId | string | null | |
| direction | receivable | payable | |
| amount | Money | |
| remaining | Money | |
| externalReference | string | null | |
| paymentReference | string | null | |
| invoiceNumber | integer | null | |
| documentDate | string | ISO date YYYY-MM-DD |
| dueDate | string | null | |
Balances
| Field | Type | Description |
| asOf | string | null | |
| from | string | null | When set, balance is the movement in [from, asOf] |
| currency | string | |
| accounts | object[] | |
| accounts[].accountId | string | |
| accounts[].number | string | |
| accounts[].name | string | |
| accounts[].type | string | |
| accounts[].reportingCode | string | null | |
| accounts[].balance | integer | |
IncomeStatement
| Field | Type | Description |
| from | string | null | |
| to | string | null | |
| currency | string | |
| lines | object[] | |
| lines[].code | string | |
| lines[].label | string | |
| lines[].amount | integer | |
| result | Money | |
BalanceSheet
| Field | Type | Description |
| asOf | string | null | |
| currency | string | |
| assets | object[] | |
| assets[].code | string | |
| assets[].label | string | |
| assets[].amount | integer | |
| equityAndLiabilities | object[] | |
| equityAndLiabilities[].code | string | |
| equityAndLiabilities[].label | string | |
| equityAndLiabilities[].amount | integer | |
| totals | object | |
| totals.assets | Money | |
| totals.equityAndLiabilities | Money | |
Settlement
| Field | Type | Description |
| id | string | |
| accountNumber | string | |
| status | string | |
| closedAt | string | null | |
| closedBy | string | null | |
| createdAt | string | RFC 3339 timestamp |
| allocations | object[] | |
| allocations[].id | string | |
| allocations[].debitPostingId | string | |
| allocations[].creditPostingId | string | |
| allocations[].amount | integer | |
| allocations[].basis | string | |
| allocations[].actor | string | |
| allocations[].createdAt | string | RFC 3339 timestamp |
SettlementProposal
| Field | Type | Description |
| id | string | |
| accountNumber | string | |
| originId | string | null | |
| allocations | object[] | |
| allocations[].debitPostingId | string | null | |
| allocations[].creditPostingId | string | null | |
| allocations[].amount | integer | |
| score | number | |
| actor | string | |
| status | string | |
| decidedBy | string | null | |
| decidedAt | string | null | |
| createdAt | string | RFC 3339 timestamp |
Payment
| Field | Type | Description |
| id | string | |
| originId | string | |
| status | string | |
| amount | Money | |
| executeOn | string | ISO date YYYY-MM-DD |
| payeeIban | string | null | |
| paymentReference | string | null | |
| bankReference | string | null | |
| rejectReason | string | null | |
| bankTransactionOriginId | string | null | |
| actor | string | |
| createdAt | string | RFC 3339 timestamp |
| updatedAt | string | RFC 3339 timestamp |
Webhook
| Field | Type | Description |
| id | string | |
| url | string | |
| events | string[] | |
| status | string | |
| createdAt | string | RFC 3339 timestamp |
Current limits
- Invoice delivery is recorded but not performed:
delivery.status stays PENDING_PROVIDER. Render and send invoices yourself for now; the number and RF reference are in the response.
- Real bank and e-invoice providers are not connected. Send bank transactions and captured bills through the origins endpoint. Payments execute only in the sandbox.
- Foreign-currency origins need an explicit
fxRate; there is no rate source yet.
- Finland only. The chart of accounts is a minimal one that covers these flows; the statutory report layouts are abbreviated to its lines.
Everything else on this page is implemented, typed in the OpenAPI document, and covered by end-to-end tests.