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_INVOICE | you | An invoice your customer issued. Vexless numbers it and assigns the RF reference. Sales |
| PURCHASE_INVOICE | you, e-invoice | A bill your customer received. Purchases |
| BANK_TRANSACTION | you, bank | One line of a statement, with what it clears. Banking |
| MANUAL | you, accountant | Postings with no external document. Postings are required; there is nothing else it could mean. Manual entries |
| SYSTEM | vexless | Generated 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. 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.settledfires on the way in. - REJECTED. A reviewer decided a
RECEIVEDorigin 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.
- Same content already received in the company, byte for byte.
- Same
externalReferenceon the same type from the same connection, or the sameexternalIdon 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.
| 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. What the document says. |
| vat | integer | The VAT amount as printed |
| dimensionIds | string[] | At most one per category |
| quantity, unit | Kept 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 | ||
| status | enum | RECEIVED · POSTED · SETTLED · REJECTED · REVERSED |
| parties | [{ role, partyId, name, externalId }] | Roles: BUYER, SELLER, PAYER, PAYEE, ISSUER. name is your company's name for the party. |
| counterpartyId | string | null | The buyer on a sale, the seller on a purchase, the payer or payee on a bank transaction |
| 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, as sent on purchase 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 interpretation behind the current entry; null while the origin is uninterpreted |
| 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, taxRateBps, dimensionIds, remaining (remaining only on open-item accounts). |
| payments | [{ id, status, amount, executeOn, bankTransactionOriginId }] | |
| interpretations | with ?explain=true | Every 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.