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"] }
ScopeAllows
origins:writeverticalSend origins and attachments, define dimensions, name parties, reverse origins you delivered
ledger:readverticalRead everything; manage webhooks
settlements:writeverticalAllocate explicitly
payments:writeverticalCreate and cancel payments; sandbox execution
connections:manageverticalStart bank connections
grants:manageownerCreate and revoke grants
review:writeaccountantAccept, reject and override interpretations; settlement proposals; close settlements with a residual; reverse any origin
accounts:manageaccountantCreate accounts, change party defaults, change auto-accept policies
close:manageaccountantLock 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.

Headers on every write

Authorizationall requestsBearer vx_…
X-ActorPOST, PUT, PATCH, DELETEWho inside your software acts: user:mikko, agent:billing@1.4. Kinds: user, agent, model, system. Recorded on everything the call produces. Missing on a write: 400 ACTOR_REQUIRED.
Idempotency-KeyPOSTUnique per logical request, kept 30 days. The key is scoped to your partner and bound to the method, the path and the body. A replay with the same method, path and body returns the stored response. A replay that differs answers 409 IDEMPOTENCY_CONFLICT with differs: "path" | "body". Missing: 400 IDEMPOTENCY_KEY_REQUIRED. Your own document id, prefixed with the company id, makes a good key.
Content-Typewith a bodyapplication/json

Money, dates, ids, paging

Moneyobject{ "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.
SignLedger 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.
DatesstringYYYY-MM-DD for business dates. RFC 3339 with milliseconds and Z for timestamps: 2026-09-03T10:16:37.767Z.
IdsstringPrefixed and time-sortable: cmp_, org_, ent_, pst_, pty_, pay_, stl_, int_, att_, fy_, whk_.
ListsEvery 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.
CurrencyBooks 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 datesSales 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…" }
StatusRule
400VALIDATION_FAILEDBody or query did not match the schema. issues[] lists every problem.
400ACTOR_REQUIRED · ACTOR_INVALID · IDEMPOTENCY_KEY_REQUIRED · AMOUNT_NOT_INTEGER
400PARTY_UNRESOLVEDNo identifier to resolve or create a party from
401UNAUTHENTICATED
403SCOPE_MISSING · COMPANY_NOT_SANDBOX
404COMPANY_NOT_FOUND · ORIGIN_NOT_FOUND · PARTY_NOT_FOUND · EXTERNAL_ACCOUNT_NOT_FOUND · POSTING_NOT_FOUND · ATTACHMENT_NOT_FOUND · FISCAL_YEAR_NOT_FOUND · NOT_FOUND
409ORIGIN_DUPLICATESame content or same external reference already received; existingId names it
409IDEMPOTENCY_CONFLICT · IDEMPOTENCY_IN_PROGRESSdiffers says whether the path or the body changed
413ATTACHMENT_TOO_LARGE10 MB decoded
422ENTRY_MUST_BALANCE · POSTING_PARTY_REQUIRED · DIMENSION_REQUIRED · DIMENSION_DUPLICATE_CATEGORYLedger invariants, mostly reachable through MANUAL origins
422ACCOUNT_NOT_FOUND · ACCOUNT_ARCHIVED · TAX_CODE_NOT_FOUND
422DATE_TOO_FAR_AHEADSee future dates above
422PERIOD_LOCKED · FISCAL_YEAR_CLOSEDAn existing entry on or before the lock date cannot change
422ORIGIN_INVALID_STATE · INTERPRETATION_INVALID_STATE · PAYMENT_INVALID_STATEThe object is not in a state that allows this action; status says which
422ORIGIN_NOT_REVERSIBLEOnly POSTED or SETTLED origins reverse, and never an invoice with payments allocated
422PAYMENT_NO_PAYEE_IBANYour company has not supplied an IBAN for the seller; add one with a party PATCH
422SETTLEMENT_INVALID · SETTLEMENT_ACCOUNT_MISMATCH · ALLOCATION_EXCEEDS_REMAINING · POSTING_NOT_OPEN_ITEM
422FISCAL_YEAR_NOT_READY · LOCK_BLOCKEDOrigins 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.

businessIdstringY-tunnus, e.g. 1234567-8
vatIdstringFI12345678
ibanstringAlso the payee account when you pay this party
einvoiceAddressstringEnables e-invoice delivery to this party
externalIdstringYour id for the party in this company; returned on every origin, open item and event
partyIdstringA pty_ id you received earlier
name, countrystringname becomes the name your company sees

Lines

descriptionstringrequired
netintegerrequiredNet in the origin currency. Negative on credit notes.
taxCodestringe.g. FI_SALE_25_5, FI_PURCHASE_14, FI_PURCHASE_EU_SERVICES. Company default when omitted.
vatintegerThe VAT amount as printed. Authoritative on purchase invoices and used to infer the tax code.
hintstringFree text such as rent, software, consulting. Mapped to an account when the pack knows it.
dimensionIdsstring[]
quantity, unitKept 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.

buyerpartyrequired
dueDatedaterequired
linesline[]requiredAt least one
externalReferencestringYour invoice id. Deduplicates and comes back on every event.
documentDatedateDefaults to today; at most 60 days ahead
currency, fxRateFor foreign-currency invoices
dataobjectAnything 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.

sellerpartyrequiredInclude iban if you will pay it through Vexless
documentDatedaterequired
linesline[]requiredSend vat per line as printed
externalReferencestringThe supplier's invoice number. Also used to recognise the payment later.
dueDatedate
totalintegerGross total as printed; refused if it does not equal lines net plus VAT
paymentReferencestringThe 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.

externalIdstringrequiredThe bank's transaction id; deduplicates
ibanstringThe company's own account. Sandbox: FI0000000000000000. Alternative: externalAccountId.
amountintegerrequiredSigned; positive is money in
bookingDatedaterequiredAt most 60 days ahead
valueDatedate
counterpartypartyUsually name and iban as the bank reports them
referencestringRF or Finnish reference on the payment; the strongest match signal
messagestringFree-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
statusenumRECEIVEDNEEDS_REVIEW | POSTEDSETTLED; 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.
counterpartyIdstring | null
documentDate, dueDatedate
currency, totalmoneyGross, in the origin currency
externalReferencestring | nullYours, or the supplier's, or the bank's
paymentReferencestring | nullRF reference; assigned on sales invoices
invoiceNumberinteger | nullSales invoices, sequential per company
lines, dataAs sent
settlementobject | null{ state: unpaid | partial | settled, paid, remaining, paidAt, paidBy[], currency }. paidBy lists the origins that paid it. Null on non-invoice origins.
deliveryobject | nullSales invoices: { channel: EINVOICE | PDF_EMAIL, status }
currentInterpretationIdstring | nullThe accepted interpretation behind the current entry; null while in review
confidencenumber | nullOf the current interpretation, or of the latest proposal while in review
attachmentsattachment[]See attachments
entriesentry[]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 }]
interpretationswith ?explain=trueEvery 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 CREATEDSENTACCEPTEDEXECUTED, 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

TypePayload keys beyond id, type, companyId, occurredAt
origin.receivedoriginId, type, externalReferenceA document arrived
origin.needs_revieworiginId, type, externalReference, confidence, notes[]An accountant will resolve it; nothing needed from you
origin.postedoriginId, type, externalReference, entryId, version, entryNumber, confidence, actorFires again with a higher version on correction
origin.settledoriginId, type, externalReference, paidAt, paid, paidBy[]Fully paid or applied
origin.rejectedoriginId, type, externalReference, reasonAn accountant decided it is not a document
origin.reversedoriginId, type, externalReference, reversalEntryId, reversesEntryId, reasonUndone; any invoice it had paid is open again
payment.acceptedpaymentId, originId, amount, bankReferenceThe bank took the transfer
payment.rejectedpaymentId, originId, reason
payment.executedpaymentId, originId, bankTransactionOriginId, amountMoney moved; the bill's origin.settled follows
company.lockedthroughEntries on or before this date are final; later origins post on the next open date
fiscal_year.closedfiscalYearId, startDate, endDate, result, closingOriginId, openingOriginIdResult moved to equity; the year is locked
invoice.delivered · connection.needs_auth · reconciliation.broken · vat_return.filedDefined; 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
FieldTypeDescription
namerequiredstring
businessIdentifierrequiredstring
jurisdictionrequiredFI
sandboxboolean
fiscalYearStartstring
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
FieldTypeDescription
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsCompanySummary[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
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
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
granteeKindrequiredVERTICAL | OFFICE | AGENT | OWNER
granteeIdrequiredstringPartner id
scopesrequiredorigins:write | ledger:read | settlements:write | payments:write | connections:manage | review:write | accounts:manage | close:manage | grants:manage[]
Response 201
FieldTypeDescription
idstring

Errors: 400401403404409413422 as { rule, message, path? }.

DELETE/v1/companies/{companyId}/grants/{grantId}

Revoke a grant · scope grants:manage

Path
FieldTypeDescription
companyIdstring
grantIdstring
Response 200
FieldTypeDescription
oktrue

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
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
dryRunboolean
Request body
type = SALES_INVOICE
FieldTypeDescription
typerequiredSALES_INVOICE
externalReferencestringYour id for the invoice
buyerrequiredobjectAny one identifier is enough; Vexless resolves or creates the party
buyer.partyIdstring
buyer.namestring
buyer.businessIdstringY-tunnus / business identifier
buyer.vatIdstring
buyer.ibanstring
buyer.einvoiceAddressstring
buyer.externalIdstringYour id for this party
buyer.countrystring
documentDatestring
dueDaterequiredstring
currencystring
fxRatenumberRequired when currency differs from the company currency
linesrequiredobject[]
lines[].descriptionrequiredstring
lines[].netrequiredintegerNet amount in the origin currency; negative on credit notes
lines[].taxCodestringe.g. FI_SALE_25_5; company default when omitted
lines[].vatintegerExplicit VAT; authoritative on purchase invoices
lines[].hintstringFree-text category hint, e.g. rent
lines[].dimensionIdsstring[]
lines[].quantitynumber
lines[].unitstring
dataobject
type = PURCHASE_INVOICE
FieldTypeDescription
typerequiredPURCHASE_INVOICE
externalReferencestringSupplier's invoice number
sellerrequiredobjectAny one identifier is enough; Vexless resolves or creates the party
seller.partyIdstring
seller.namestring
seller.businessIdstringY-tunnus / business identifier
seller.vatIdstring
seller.ibanstring
seller.einvoiceAddressstring
seller.externalIdstringYour id for this party
seller.countrystring
documentDaterequiredstring
dueDatestring
currencystring
fxRatenumber
linesrequiredobject[]
lines[].descriptionrequiredstring
lines[].netrequiredintegerNet amount in the origin currency; negative on credit notes
lines[].taxCodestringe.g. FI_SALE_25_5; company default when omitted
lines[].vatintegerExplicit VAT; authoritative on purchase invoices
lines[].hintstringFree-text category hint, e.g. rent
lines[].dimensionIdsstring[]
lines[].quantitynumber
lines[].unitstring
totalintegerGross total as printed; validated against lines
paymentReferencestringSupplier's payment reference (RF or Finnish)
dataobject
type = BANK_TRANSACTION
FieldTypeDescription
typerequiredBANK_TRANSACTION
externalIdrequiredstringThe bank's transaction id
externalAccountIdstring
ibanstringThe company's own account; alternative to externalAccountId
amountrequiredintegerSigned; positive = money in
bookingDaterequiredstring
valueDatestring
counterpartyobjectAny one identifier is enough; Vexless resolves or creates the party
counterparty.partyIdstring
counterparty.namestring
counterparty.businessIdstringY-tunnus / business identifier
counterparty.vatIdstring
counterparty.ibanstring
counterparty.einvoiceAddressstring
counterparty.externalIdstringYour id for this party
counterparty.countrystring
referencestring
messagestring
currencystring
fxRatenumber
settlesobject[]Origins this transaction settles, when your software knows
settles[].originIdrequiredstring
settles[].amountintegerInteger minor units (cents)
type = MANUAL
FieldTypeDescription
typerequiredMANUAL
descriptionrequiredstring
documentDaterequiredstring
externalReferencestring
counterpartyobjectAny one identifier is enough; Vexless resolves or creates the party
counterparty.partyIdstring
counterparty.namestring
counterparty.businessIdstringY-tunnus / business identifier
counterparty.vatIdstring
counterparty.ibanstring
counterparty.einvoiceAddressstring
counterparty.externalIdstringYour id for this party
counterparty.countrystring
postingsrequiredobject[]
postings[].accountNumberrequiredstring
postings[].amountrequiredintegerInteger minor units (cents)
postings[].partyIdstring
postings[].taxCodestring
postings[].dimensionIdsstring[]
postings[].descriptionstring
dataobject
Response 200
FieldTypeDescription
idstring
typestring
statusstringRECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED
partiesobject[]
parties[].rolestring
parties[].partyIdstring
parties[].namestring | null
parties[].externalIdstring | null
counterpartyIdstring | null
documentDatestringISO date YYYY-MM-DD
dueDatestring | null
currencystring
totalMoney
externalReferencestring | null
paymentReferencestring | null
invoiceNumberinteger | null
linesobject[]
dataobject
settlementSettlementState | null
settlement.stateunpaid | partial | settled
settlement.paidinteger
settlement.remaininginteger
settlement.paidAtstring | null
settlement.paidBystring[]Origins that paid this one
settlement.currencystring
deliveryobject | null
delivery.channelstring
delivery.statusstring
currentInterpretationIdstring | null
confidencenumber | null
attachmentsAttachment[]
createdBystring
createdAtstringRFC 3339 timestamp
updatedAtstringRFC 3339 timestamp
entriesEntry[]
paymentsobject[]
payments[].idstring
payments[].statusstring
payments[].amountMoney
payments[].executeOnstringISO date YYYY-MM-DD
payments[].bankTransactionOriginIdstring | null
interpretationsInterpretation[]Present with ?explain=true
dryRuntrue
Response 201 · Origin

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/origins

List origins · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
typeSALES_INVOICE | PURCHASE_INVOICE | BANK_TRANSACTION | MANUAL | SYSTEM | PAYROLL
statusRECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED
partyIdstring
fromstring
tostring
externalReferencestring
settlementStateunpaid | partial | settled
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsOrigin[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
originIdstring
Query
FieldTypeDescription
explainboolean
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
FieldTypeDescription
companyIdstring
originIdstring
Request body
FieldTypeDescription
reasonstring
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
FieldTypeDescription
companyIdstring
originIdstring
Request body
FieldTypeDescription
filenamerequiredstring
mediaTyperequiredstringe.g. application/pdf, image/jpeg
contentBase64requiredstringFile content, base64; at most 10 MB decoded
Response 201 · Attachment

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/origins/{originId}/attachments

List an origin's attachments · scope ledger:read

Path
FieldTypeDescription
companyIdstring
originIdstring
Response 200
FieldTypeDescription
itemsAttachment[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
originIdstring
attachmentIdstring
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
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
fromstring
tostring
accountstring
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsEntry[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
entryIdstring
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
FieldTypeDescription
companyIdstring
entryIdstring
Response 200
FieldTypeDescription
itemsEntry[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
asOfstring
fromstringWith from, balances are movements in [from, asOf]
fiscalYearIdstringShorthand for from and asOf of that year
accountsstringComma-separated account numbers
dimensionIdstring
Response 200 · Balances

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/open-items

Open receivables and payables · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
directionreceivable | payable
partyIdstring
dueBeforestring
accountstring
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsOpenItem[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
fromstring
tostring
fiscalYearIdstring
Response 200 · IncomeStatement

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/reports/balance-sheet

Balance sheet at a date · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
asOfstring
Response 200 · BalanceSheet

Errors: 400401403404409413422 as { rule, message, path? }.

Settlements

POST/v1/companies/{companyId}/settlements

Allocate open items explicitly · scope settlements:write

Path
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
allocationsrequiredobject[]
allocations[].debitPostingIdrequiredstring
allocations[].creditPostingIdrequiredstring
allocations[].amountrequiredintegerInteger minor units (cents)
Response 201 · Settlement

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/settlements/{settlementId}

Get a settlement with its allocations · scope ledger:read

Path
FieldTypeDescription
companyIdstring
settlementIdstring
Response 200 · Settlement

Errors: 400401403404409413422 as { rule, message, path? }.

POST/v1/companies/{companyId}/settlements/{settlementId}/close

Close a settlement by booking the residual · scope review:write

Path
FieldTypeDescription
companyIdstring
settlementIdstring
Request body
FieldTypeDescription
kindWRITE_OFF | FX_DIFFERENCE | FEE | YEAR_END | VAT_SETTLEMENT
datestring
Response 200 · Settlement

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
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
originIdrequiredstring
amountintegerInteger minor units (cents)
executeOnstring
Response 201 · Payment

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/payments/{paymentId}

Get a payment · scope ledger:read

Path
FieldTypeDescription
companyIdstring
paymentIdstring
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
FieldTypeDescription
companyIdstring
paymentIdstring
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
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
externalIdstring
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsParty[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
partyIdstring
Request body
FieldTypeDescription
namestringThe name this company knows the party by
externalIdstring | null
defaultAccountNumberstring | null
defaultTaxCodestring | null
ibanstring
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
FieldTypeDescription
companyIdstring
Response 200
FieldTypeDescription
itemsAccount[]
nextstring | nullCursor for the next page, or null

Errors: 400401403404409413422 as { rule, message, path? }.

POST/v1/companies/{companyId}/accounts

Add an account · scope accounts:manage

Path
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
numberrequiredstring
namerequiredstring
typerequiredASSET | LIABILITY | EQUITY | REVENUE | EXPENSE
openItemsboolean
reportingCodestring
Response 201
FieldTypeDescription
idstring

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/tax-codes

List tax codes for a jurisdiction · scope any key

Query
FieldTypeDescription
jurisdictionstring
Response 200
FieldTypeDescription
itemsTaxCode[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Response 200
FieldTypeDescription
itemsDimensionCategory[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
categoryrequiredstring
namerequiredstring
requiredOnAccountsobject[]
requiredOnAccounts[].fromrequiredstring
requiredOnAccounts[].torequiredstring
Response 201
FieldTypeDescription
idstring
categoryIdstring

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/policies/auto-accept

List auto-accept policies per origin type · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Response 200
FieldTypeDescription
itemsAutoAcceptPolicy[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
originTypeSALES_INVOICE | PURCHASE_INVOICE | BANK_TRANSACTION | MANUAL | SYSTEM | PAYROLL
Request body
FieldTypeDescription
minConfidencerequirednumber
maxAmountinteger | nullInteger minor units (cents)
knownPartyOnlyrequiredboolean
Response 200
FieldTypeDescription
itemsAutoAcceptPolicy[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
kindrequiredBANK
providerrequiredstring
Response 201
FieldTypeDescription
idstring
statusstring
consentUrlstring | null

Errors: 400401403404409413422 as { rule, message, path? }.

Webhooks

POST/v1/companies/{companyId}/webhooks

Subscribe a URL to events · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
urlrequiredstring (uri)
eventsrequiredorigin.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[]
secretstring
Response 201
FieldTypeDescription
idstring
secretstring

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/webhooks

List webhooks · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Response 200
FieldTypeDescription
itemsWebhook[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
webhookIdstring
Response 200
FieldTypeDescription
oktrue

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
FieldTypeDescription
companyIdstring
originIdstring
Request body
FieldTypeDescription
reasonstring
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
FieldTypeDescription
companyIdstring
originIdstring
Request body
FieldTypeDescription
postingsrequiredobject[]
postings[].accountNumberrequiredstring
postings[].amountrequiredintegerInteger minor units (cents)
postings[].partyIdstring
postings[].taxCodestring
postings[].dimensionIdsstring[]
postings[].descriptionstring
reasonstring
Response 201 · Origin

Errors: 400401403404409413422 as { rule, message, path? }.

POST/v1/companies/{companyId}/interpretations/{interpretationId}/accept

Accept a proposed interpretation · scope review:write

Path
FieldTypeDescription
companyIdstring
interpretationIdstring
Request body
FieldTypeDescription
reasonstring
Response 200 · Origin

Errors: 400401403404409413422 as { rule, message, path? }.

POST/v1/companies/{companyId}/interpretations/{interpretationId}/reject

Reject a proposed interpretation · scope review:write

Path
FieldTypeDescription
companyIdstring
interpretationIdstring
Request body
FieldTypeDescription
reasonstring
Response 200
FieldTypeDescription
oktrue

Errors: 400401403404409413422 as { rule, message, path? }.

GET/v1/companies/{companyId}/settlement-proposals

List settlement proposals · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Query
FieldTypeDescription
statusPROPOSED | ACCEPTED | REJECTED | SUPERSEDED
cursorstring
limitinteger
Response 200
FieldTypeDescription
itemsSettlementProposal[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
proposalIdstring
Response 200
FieldTypeDescription
oktrue

Errors: 400401403404409413422 as { rule, message, path? }.

POST/v1/companies/{companyId}/settlement-proposals/{proposalId}/reject

Reject a settlement proposal · scope review:write

Path
FieldTypeDescription
companyIdstring
proposalIdstring
Response 200
FieldTypeDescription
oktrue

Errors: 400401403404409413422 as { rule, message, path? }.

Close (accountant scopes)

GET/v1/companies/{companyId}/fiscal-years

List fiscal years · scope ledger:read

Path
FieldTypeDescription
companyIdstring
Response 200
FieldTypeDescription
itemsFiscalYear[]
nextstring | nullCursor 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
FieldTypeDescription
companyIdstring
Request body
FieldTypeDescription
throughrequiredstring
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
FieldTypeDescription
companyIdstring
fiscalYearIdstring
Response 200 · FiscalYear

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
FieldTypeDescription
companyIdstring
paymentIdstring
Request body
FieldTypeDescription
bookingDatestringDefaults 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
FieldTypeDescription
companyIdstring
paymentIdstring
Request body
FieldTypeDescription
reasonstring
Response 200 · Payment

Errors: 400401403404409413422 as { rule, message, path? }.

Meta

GET/health

Liveness check · scope none

Response 200
FieldTypeDescription
okboolean

Schemas

Named objects the responses refer to. Every field is present in every response; nullable fields are marked.

Money

FieldTypeDescription
amountintegerInteger minor units
currencystring

Error

FieldTypeDescription
rulestring
messagestring
pathstring

Company

FieldTypeDescription
idstring
namestring
businessIdentifierstring
jurisdictionstring
currencystring
lockDatestring | null
sandboxboolean
createdAtstringRFC 3339 timestamp
connectionsobject[]
connections[].idstring
connections[].kindstring
connections[].providerstring
connections[].statusstring
connections[].consentUrlstring | null
externalAccountsobject[]
externalAccounts[].idstring
externalAccounts[].connectionIdstring
externalAccounts[].kindstring
externalAccounts[].externalIdstring
externalAccounts[].accountNumberstring
fiscalYearsFiscalYear[]

CompanySummary

FieldTypeDescription
idstring
namestring
businessIdentifierstring
jurisdictionstring
currencystring
lockDatestring | null
sandboxboolean
createdAtstringRFC 3339 timestamp

FiscalYear

FieldTypeDescription
idstring
startDatestringISO date YYYY-MM-DD
endDatestringISO date YYYY-MM-DD
closedAtstring | null
closedBystring | null
resultAmountinteger | null
closingOriginIdstring | null
openingOriginIdstring | null

Account

FieldTypeDescription
idstring
numberstring
namestring
typestring
openItemsboolean
statusstring
reportingCodestring | null
createdAtstringRFC 3339 timestamp

TaxCode

FieldTypeDescription
idstring
jurisdictionstring
codestring
namestring
kindstring
rateBpsinteger
validFromstringISO date YYYY-MM-DD
validTostring | null
returnLinestring
vatAccountNumberstring

DimensionCategory

FieldTypeDescription
idstring
namestring
requiredOnAccountsobject[]
requiredOnAccounts[].fromstring
requiredOnAccounts[].tostring
dimensionsobject[]
dimensions[].idstring
dimensions[].namestring

AutoAcceptPolicy

FieldTypeDescription
originTypestring
minConfidencenumber
maxAmountinteger | null
knownPartyOnlyboolean

Party

FieldTypeDescription
idstringRelationship id; the paging cursor
partyIdstring
namestringThe name this company knows the party by
countrystring | null
rolesstring[]
externalIdstring | null
defaultAccountNumberstring | null
defaultTaxCodestring | null
identifiersobject[]Only identifiers this company supplied
identifiers[].schemestring
identifiers[].valuestring
identifiers[].sourcestring
createdAtstringRFC 3339 timestamp

Origin

FieldTypeDescription
idstring
typestring
statusstringRECEIVED | NEEDS_REVIEW | POSTED | SETTLED | REJECTED | REVERSED
partiesobject[]
parties[].rolestring
parties[].partyIdstring
parties[].namestring | null
parties[].externalIdstring | null
counterpartyIdstring | null
documentDatestringISO date YYYY-MM-DD
dueDatestring | null
currencystring
totalMoney
externalReferencestring | null
paymentReferencestring | null
invoiceNumberinteger | null
linesobject[]
dataobject
settlementSettlementState | null
settlement.stateunpaid | partial | settled
settlement.paidinteger
settlement.remaininginteger
settlement.paidAtstring | null
settlement.paidBystring[]Origins that paid this one
settlement.currencystring
deliveryobject | null
delivery.channelstring
delivery.statusstring
currentInterpretationIdstring | null
confidencenumber | null
attachmentsAttachment[]
createdBystring
createdAtstringRFC 3339 timestamp
updatedAtstringRFC 3339 timestamp
entriesEntry[]
paymentsobject[]
payments[].idstring
payments[].statusstring
payments[].amountMoney
payments[].executeOnstringISO date YYYY-MM-DD
payments[].bankTransactionOriginIdstring | null
interpretationsInterpretation[]Present with ?explain=true

Entry

FieldTypeDescription
idstring
versioninteger
numberinteger
datestringISO date YYYY-MM-DD
descriptionstring
originIdstring
interpretationIdstring
actorstring
isCurrentboolean
reversesEntryIdstring | null
createdAtstringRFC 3339 timestamp
postingsPosting[]

Posting

FieldTypeDescription
idstring
accountIdstring
accountNumberstring
accountNamestring
amountintegerBook currency, signed; debit positive
currencystring
originalAmountinteger | null
originalCurrencystring | null
fxRatenumber | null
partyIdstring | null
taxCodestring | null
dimensionIdsstring[]
descriptionstring | null
remaininginteger | nullOpen-item accounts only; signed like amount

Attachment

FieldTypeDescription
idstring
originIdstring
filenamestring
mediaTypestring
sizeinteger
hashstring
retentionUntilstring | null
createdBystring
createdAtstringRFC 3339 timestamp

Interpretation

FieldTypeDescription
idstring
versioninteger
actorstring
confidencenumber
statusstring
notesstring[]
postingsobject[]
decidedBystring | null
decidedAtstring | null
reasonstring | null
createdAtstringRFC 3339 timestamp

SettlementState

FieldTypeDescription
stateunpaid | partial | settled
paidinteger
remaininginteger
paidAtstring | null
paidBystring[]Origins that paid this one
currencystring

OpenItem

FieldTypeDescription
idstringPosting id; the paging cursor
postingIdstring
originIdstring
originTypestring
entryIdstring
accountNumberstring
accountNamestring
partyIdstring | null
partyNamestring | null
partyExternalIdstring | null
directionreceivable | payable
amountMoney
remainingMoney
externalReferencestring | null
paymentReferencestring | null
invoiceNumberinteger | null
documentDatestringISO date YYYY-MM-DD
dueDatestring | null

Balances

FieldTypeDescription
asOfstring | null
fromstring | nullWhen set, balance is the movement in [from, asOf]
currencystring
accountsobject[]
accounts[].accountIdstring
accounts[].numberstring
accounts[].namestring
accounts[].typestring
accounts[].reportingCodestring | null
accounts[].balanceinteger

IncomeStatement

FieldTypeDescription
fromstring | null
tostring | null
currencystring
linesobject[]
lines[].codestring
lines[].labelstring
lines[].amountinteger
resultMoney

BalanceSheet

FieldTypeDescription
asOfstring | null
currencystring
assetsobject[]
assets[].codestring
assets[].labelstring
assets[].amountinteger
equityAndLiabilitiesobject[]
equityAndLiabilities[].codestring
equityAndLiabilities[].labelstring
equityAndLiabilities[].amountinteger
totalsobject
totals.assetsMoney
totals.equityAndLiabilitiesMoney

Settlement

FieldTypeDescription
idstring
accountNumberstring
statusstring
closedAtstring | null
closedBystring | null
createdAtstringRFC 3339 timestamp
allocationsobject[]
allocations[].idstring
allocations[].debitPostingIdstring
allocations[].creditPostingIdstring
allocations[].amountinteger
allocations[].basisstring
allocations[].actorstring
allocations[].createdAtstringRFC 3339 timestamp

SettlementProposal

FieldTypeDescription
idstring
accountNumberstring
originIdstring | null
allocationsobject[]
allocations[].debitPostingIdstring | null
allocations[].creditPostingIdstring | null
allocations[].amountinteger
scorenumber
actorstring
statusstring
decidedBystring | null
decidedAtstring | null
createdAtstringRFC 3339 timestamp

Payment

FieldTypeDescription
idstring
originIdstring
statusstring
amountMoney
executeOnstringISO date YYYY-MM-DD
payeeIbanstring | null
paymentReferencestring | null
bankReferencestring | null
rejectReasonstring | null
bankTransactionOriginIdstring | null
actorstring
createdAtstringRFC 3339 timestamp
updatedAtstringRFC 3339 timestamp

Webhook

FieldTypeDescription
idstring
urlstring
eventsstring[]
statusstring
createdAtstringRFC 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.