Understand Vexless

Origins

The document behind every entry. One endpoint, four types, one lifecycle.

What an origin is

Everything in the ledger traces back to a document: an invoice issued, a bill received, a line on a bank statement. That document is an origin. Vexless keeps it as sent, resolves the parties on it, deduplicates it, and holds it until someone says what it means. When they do, the entry that results names the origin, and the origin lists its entries. One chain explains every posting:

Posting → Entry → Interpretation → Origin

Every entry has exactly one origin. Every posted origin has at least one entry. There is no entry without a document behind it, and no draft: an origin is a document that was issued or received, not one being edited.

Types

SALES_INVOICEyouAn invoice your customer issued. Vexless numbers it and assigns the RF reference. Sales
PURCHASE_INVOICEyou, e-invoiceA bill your customer received. Purchases
BANK_TRANSACTIONyou, bankOne line of a statement, with what it clears. Banking
MANUALyou, accountantPostings with no external document. Postings are required; there is nothing else it could mean. Manual entries
SYSTEMvexlessGenerated by Vexless: write-off, FX difference, fee, year-end close. Read-only; data holds the computation.

The four you send share one endpoint, discriminated by type. Each type has its own fields, listed on its guide; they share lines, parties, postings and the response.

POST /v1/companies/{companyId}/origins                   # 201, or 409 ORIGIN_DUPLICATE
GET  /v1/companies/{companyId}/origins/{originId}?explain=true
GET  /v1/companies/{companyId}/origins?type=&status=&partyId=&from=&to=&externalReference=&settlementState=&cursor=&limit=

Lifecycle

RECEIVED→POSTED⇄SETTLED
  • RECEIVED. The document is in, parties are resolved, nobody has said what it means. Sent with postings, an origin skips this state.
  • POSTED. The first interpretation produced an entry. Open items exist. origin.posted.
  • SETTLED. Every open item the origin created has been cleared. Recomputed on every allocation change, so a released allocation moves it back to POSTED. origin.settled fires on the way in.
  • REJECTED. A reviewer decided a RECEIVED origin is not a document. Terminal. origin.rejected.
  • REVERSED. A posted origin was undone with a negating entry. Terminal. origin.reversed.

Non-invoice origins have no open items to settle, so a bank transaction or manual entry ends at POSTED and its settlement is null.

Deduplication

Applied at ingest, in order. A duplicate answers 409 ORIGIN_DUPLICATE with existingId, so a retried feed never books twice.

  1. Same content already received in the company, byte for byte.
  2. Same externalReference on the same type from the same connection, or the same externalId on a bank transaction. A rejected origin does not count, so its reference can be sent again.

Your idempotency key guards the request; deduplication guards the document. Both matter: a retried request with a new key that carries the same invoice is caught here.

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. Sales and purchase invoices require at least one.

descriptionstringrequired
netintegerrequiredNet in the origin currency. Negative on credit notes.
taxCodestringe.g. FI_SALE_25_5, FI_PURCHASE_14, FI_PURCHASE_EU_SERVICES. What the document says.
vatintegerThe VAT amount as printed
dimensionIdsstring[]At most one per category
quantity, unitKept for invoice rendering

A total, when sent, must equal the sum of net plus VAT across the lines; otherwise the origin is refused.

Dates

documentDate is the document's own date. A bank transaction's is its bookingDate; a sales invoice's defaults to today. The entry's date is the posting date: the document date, or the first open date after the company's lock when the document is late. Sales invoices, purchase invoices and bank transactions dated more than 60 days ahead are refused with DATE_TOO_FAR_AHEAD; manual entries are exempt.

The origin response

Returned by every origin endpoint, and carried in part by every origin event.

id, type
statusenumRECEIVED · POSTED · SETTLED · REJECTED · REVERSED
parties[{ role, partyId, name, externalId }]Roles: BUYER, SELLER, PAYER, PAYEE, ISSUER. name is your company's name for the party.
counterpartyIdstring | nullThe buyer on a sale, the seller on a purchase, the payer or payee on a bank transaction
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, as sent on purchase 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 interpretation behind the current entry; null while the origin is uninterpreted
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, taxRateBps, dimensionIds, remaining (remaining only on open-item accounts).
payments[{ id, status, amount, executeOn, bankTransactionOriginId }]
interpretationswith ?explain=trueEvery version: actor, status (ACCEPTED | SUPERSEDED), postings[], reason. Enough to tell a user who put a bill where it is.
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.

Dry runs

Add ?dryRun=true to the POST. The whole pipeline runs, party resolution, deduplication, posting, allocation, and is rolled back. The response is the origin as it would have been, with "dryRun": true and the interpretations included, and status 200 rather than 201. Refusals come back as they would for real. Nothing is stored, no event fires, and the idempotency key is not consumed.

Reversing an origin

A bank feed can push the same transaction twice under different ids, or a captured bill can be simply wrong. Reversal books a new entry with the postings negated, releases the original's allocations so that any invoice it had paid is open again, and sets the origin to REVERSED. The original entry stays as history and both entries appear on the origin.

POST /v1/companies/{companyId}/origins/{originId}/reverse
{ "reason": "Duplicate of bank-tx-8812" }        # body optional

With origins:write you may reverse origins your own partner delivered; review:write may reverse any. Only POSTED or SETTLED origins reverse, and never an invoice that has payments allocated to it: issue a credit note, or have a reviewer correct it. Otherwise 422 ORIGIN_NOT_REVERSIBLE.

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. Nothing is deleted in this version.

Listing and filtering

GET /v1/companies/{companyId}/origins?type=PURCHASE_INVOICE&status=RECEIVED          # bills waiting for an interpretation
GET /v1/companies/{companyId}/origins?type=SALES_INVOICE&settlementState=unpaid     # unpaid invoices, as origins
GET /v1/companies/{companyId}/origins?partyId=pty_…&from=2026-01-01&to=2026-06-30   # one party, by document date
GET /v1/companies/{companyId}/origins?externalReference=2026-000117                 # by your id

Lists return { items, next }; pass next back as cursor. limit is 1 to 200, default 50.