Get started

Quickstart

Six requests take one customer from nothing to a paid invoice. All of them work on a sandbox company with no real bank involved.

Before you start

Every request carries your API key. Every write also names who inside your software is acting, and every POST carries a key that makes it safe to retry.

Authorizationall requestsBearer vx_…. See Authentication.
X-Actorall writesuser:mikko or agent:billing@1.4. Recorded on everything the call produces.
Idempotency-Keyevery POSTUnique per logical request. See Idempotency.
Content-Typewith a bodyapplication/json

The examples below show the headers on the first request and leave them out afterwards.

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. One key serves every company you create; sandbox is a property of the company, not of the key.

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 }
{ "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.received", "origin.posted", "origin.settled", "payment.executed", "payment.rejected"] }
{ "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, "vat": 25500, "taxCode": "FI_SALE_25_5" } ],
  "postings": [
    { "accountNumber": "1700", "amount":  125500 },
    { "accountNumber": "3010", "amount": -100000, "taxCode": "FI_SALE_25_5" },
    { "accountNumber": "2939", "amount":  -25500, "taxCode": "FI_SALE_25_5" } ]
}
{ "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_…",
  "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 the document, the postings and a business id. Vexless resolved the buyer, checked that the postings balance and that the receivable carries a party, assigned the invoice number and RF reference, and posted the entry. The receivable took the buyer as its party without you repeating the id: postings with no partyId inherit the origin's counterparty.

Do not know the accounts? Leave postings out. The origin waits at RECEIVED until an accountant interprets it, and you get origin.posted when they do. See Interpretations.

5. Show what is open

GET /v1/companies/cmp_06g6…/open-items?direction=receivable
{ "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 }

In production the bank connection delivers the transaction. In the sandbox, and for any bank feed you operate yourself, you send it, and you name the invoice it pays:

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" },
  "postings": [ { "accountNumber": "1910", "amount":  125500 },
                { "accountNumber": "1700", "amount": -125500, "partyId": "pty_…" } ],
  "settles": [ { "originId": "org_06g6…" } ] }
{ "id": "org_…", "type": "BANK_TRANSACTION", "status": "POSTED", … }   # the 1700 posting is allocated against the invoice you named

Your webhook receives origin.settled for the invoice with your external reference. Its status is now SETTLED. That is the whole loop.

POST https://your.app/vexless
X-Vexless-Event: origin.settled
X-Vexless-Signature: t=1757000000,v1=…

{ "id": "evt_…", "type": "origin.settled", "companyId": "cmp_06g6…", "occurredAt": "2026-09-15T09:12:03.120Z",
  "originId": "org_06g6…", "externalReference": "2026-000117", "paidAt": "2026-09-15",
  "paid": { "amount": 125500, "currency": "EUR" }, "paidBy": ["org_…"] }

Where next