Build with Vexless
Sales
Issue invoices from your software, let Vexless number and reference them, and learn when they are paid.
What Vexless does with a sales invoice
- Assigns the invoice number, sequential and gapless per company, and the RF payment reference the buyer puts on the transfer.
- Resolves the buyer to a party from any identifier you send.
- Posts the entry from the postings you send, or holds the invoice at
RECEIVEDuntil someone else interprets it. - Tracks what is paid: the receivable is an open item, and
settlementon the origin says how much of it has been cleared. - Records the delivery channel. Delivery itself is not performed in this version; see delivery.
- Fires
origin.postedand, when the money arrives,origin.settled.
Issue an invoice
One request. The document, the buyer, and what it means in the ledger.
POST /v1/companies/{companyId}/origins
Idempotency-Key: {companyId}-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", "quantity": 1, "unit": "pcs" } ],
"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_…", "type": "SALES_INVOICE", "status": "POSTED",
"invoiceNumber": 117, "paymentReference": "RF74000117",
"parties": [ { "role": "BUYER", "partyId": "pty_…", "name": "Ostaja Oy", "externalId": "cust-42" } ],
"documentDate": "2026-09-03", "dueDate": "2026-09-17",
"total": { "amount": 125500, "currency": "EUR" },
"settlement": { "state": "unpaid", "paid": 0, "remaining": 125500, "paidAt": null, "paidBy": [], "currency": "EUR" },
"delivery": { "channel": "PDF_EMAIL", "status": "PENDING_PROVIDER" },
"entries": [ { "number": 214, "version": 1, … } ], … }
The three postings are the invoice as the ledger sees it: the receivable, the revenue, and the VAT. They must sum to zero. The receivable is on an open-item account, so it needs a party; a posting with no partyId takes the origin's counterparty, here the buyer. Interpretations explains the rules in full.
Fields
| buyer | partyrequired | Any one identifier. See naming a party. |
| 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 |
| total | integer | Gross total as issued; refused if it does not equal lines net plus VAT |
| postings | posting[] | Omit to leave the invoice awaiting an interpretation |
| currency, fxRate | For foreign-currency invoices; see below | |
| data | object | Anything you want stored with the document. Returned as sent. |
Lines
What the document said, kept as sent. Lines are not turned into postings; they are the record of the document and the basis of its total.
| description | stringrequired | |
| net | integerrequired | Net in the origin currency, minor units. Negative on credit notes. |
| taxCode | string | e.g. FI_SALE_25_5, FI_SALE_EU_SERVICES. What the document says. |
| vat | integer | The VAT amount as printed |
| dimensionIds | string[] | At most one per category. See dimensions. |
| quantity, unit | Kept for invoice rendering |
Issue it without postings
If your software does not know which accounts an invoice hits, send the invoice without postings. It still gets its number and RF reference, and it waits at RECEIVED. An accountant with review:write, an agent, or your own software later sends the interpretation, and you see origin.posted when it happens.
POST /v1/companies/{companyId}/origins/{originId}/interpretations
{ "postings": [ { "accountNumber": "1700", "amount": 125500 }, { "accountNumber": "3010", "amount": -100000, "taxCode": "FI_SALE_25_5" }, { "accountNumber": "2939", "amount": -25500, "taxCode": "FI_SALE_25_5" } ] }
Nothing is guessed in the meantime. There is no default account for a customer, no rule that reads the lines. The invoice is a document until somebody says what it means.
Track what is open
GET /v1/companies/{companyId}/open-items?direction=receivable&partyId=pty_…&dueBefore=2026-09-30
{ "items": [ { "originId": "org_…", "invoiceNumber": 117, "externalReference": "2026-000117", "paymentReference": "RF74000117",
"partyName": "Ostaja Oy", "partyExternalId": "cust-42", "direction": "receivable",
"amount": { "amount": 125500, "currency": "EUR" }, "remaining": { "amount": 125500, "currency": "EUR" },
"documentDate": "2026-09-03", "dueDate": "2026-09-17", … } ],
"next": null }
Filter by partyId for one customer's statement, by dueBefore for what is overdue. GET /origins?type=SALES_INVOICE&settlementState=unpaid gives the same invoices as full origins.
Get paid
Money arrives as a BANK_TRANSACTION origin. In production the bank connection sends it; today you do, and you name the invoice it pays in settles. Vexless allocates the transaction's receivable posting against the invoice's, marks the allocation EXPLICIT, and fires origin.settled for the invoice.
X-Vexless-Event: origin.settled
{ "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…"] }
A partial payment leaves the invoice at POSTED with settlement.state: "partial" and the remainder open. Banking shows the transaction side; Settlements explains partial payments and overpayments.
Credit notes
A credit note is a sales invoice with negative lines. It gets its own invoice number, and its postings credit the receivable. Clear it against the original with an explicit settlement between the two receivable postings; both then show as settled.
POST /v1/companies/{companyId}/origins
{ "type": "SALES_INVOICE", "externalReference": "2026-000118", "buyer": { "partyId": "pty_…" }, "dueDate": "2026-09-17",
"lines": [ { "description": "Credit: 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" } ],
"data": { "credits": "2026-000117" } }
POST /v1/companies/{companyId}/settlements
{ "allocations": [ { "debitPostingId": "pst_invoice…", "creditPostingId": "pst_creditnote…", "amount": 125500 } ] }
Delivery
Every sales invoice carries delivery: { channel, status }. The channel is EINVOICE when the buyer has an e-invoice address, otherwise PDF_EMAIL. In this version delivery is recorded and not performed: status stays PENDING_PROVIDER. Render and send the invoice yourself; the number and RF reference are in the response, and invoice.delivered is defined for when a provider is connected.
Foreign currency
Books are kept in the company currency, EUR. An invoice in another currency carries currency and fxRate, book units per origin unit. Line amounts and total are in the origin currency; postings are in book currency, and the original amount is kept on each posting. There is no rate source yet, so the rate is yours to supply.
{ "type": "SALES_INVOICE", "currency": "SEK", "fxRate": 0.0885, "lines": [ { "description": "…", "net": 1000000, "taxCode": "FI_SALE_EU_SERVICES" } ],
"postings": [ { "accountNumber": "1700", "amount": 88500 }, { "accountNumber": "3400", "amount": -88500, "taxCode": "FI_SALE_EU_SERVICES" } ], … }
Corrections
An invoice that was wrong is credited, as above. An invoice that was booked wrong is re-interpreted by a reviewer: a new interpretation version replaces the entry under the same number and origin.posted fires again with a higher version. An invoice that should never have existed and has no payment allocated to it can be reversed.
Events you will see
| origin.received | The invoice arrived without postings and is waiting | |
| origin.posted | Posted, or re-posted with a higher version | |
| origin.settled | Fully paid or applied. Carries paid, paidAt, paidBy[] | |
| origin.reversed | Undone | |
| invoice.delivered | Defined; not emitted in this version |