Transactions
Build transaction forms with valid types, party, payment, and locks
Mental Model
Transactions record financial activity that is not represented solely by a reservation. Their lines provide the account, amount, attribution, and related resource context from which VRPlatform creates balanced journal entries.
party means accounting attribution. It does not identify who created, paid,
or approved a transaction.
Resources and Lifecycle
Create transaction (POST /transactions)
accepts four public types:
| Type | Purpose | Primary shape |
|---|---|---|
deposit | Money received or matched to guest/channel activity | One or more lines |
expense | Cost or bill | Contact, optional payment, markup, attachments |
transfer | Movement between internal banks | One line plus root source account |
payout | Owner or manager distribution | One line, optional statement link |
Transactions can be active or inactive and remain in history after archive. Recurring transaction templates create scheduled expenses and have their own active state, recurrence, preview, and run lifecycle.
Read Model
Render the returned transaction, payment, line attribution, reconciled bank records, attachments, calculated totals, and granular locks. Use resolved listing context from a linked reservation rather than preserving a competing manual listing selection.
Use integer cents. Transaction totals are the sum of line amounts. The server normalizes transfer source direction, and a transfer without a description gets a generated one ("Transfer from ... to ..."); every other type requires an explicit description.
Decision Table
Party Defaults and Options
| Transaction and context | Default | Valid public options |
|---|---|---|
| Deposit without listing or reservation | manager | manager |
| Deposit with listing or reservation | owners | Both unless a forced rule below applies |
| Opening-balance deposit | Context default | Both |
| Expense | manager | Both unless a forced rule below applies |
| Strict internal transfer | Party-neutral bank postings | Hide control |
| Payout | owners | Both |
"Context default" means the same default as a normal deposit in that context:
manager without listing or reservation context, owners with it.
owners participates in ownership allocation, owner balances, statements, and
owner-side locks. manager records manager-side activity and normally avoids
owner-statement period and inactive-ownership guards. The internal journal
party none is not valid transaction-line input.
Recalculate visible options whenever type, account, system assignment, category, listing, reservation, or owner-statement context changes. A reservation matcher is not owner context until it resolves.
Forced Party Rules
- Lodging or occupancy tax payable forces
ownersfor deposits and expenses. deposit_coHostPayoutforcesowners.- Resolved system accounts-receivable and deferred-revenue accounts force
owners. deposit_reserveforcesmanager.- Merchant-fee, channel-fee, and VAT deposits default to
manager; an explicitownersvalue needs listing or reservation context.
Custom accounts with similar categories do not automatically inherit a forced system-assignment rule.
Type Constraints
- Deposits do not accept line markup. Non-expense transactions, including
opening-balance deposits, are always marked paid on the transaction
date(the stored date when an update omits it); only expenses carry a separate payment state. - Expenses require
contactIdunless they are opening balances. They alone acceptattachmentIds; their lines can carry markup and tax application. - Transfers require exactly one line, two different internal bank accounts, no statement link, and no markup. The root account is the source and line account is the destination.
- Payouts require exactly one line. Statement linkage belongs on the line as
ownerStatementId.
Editability
Transaction reads expose:
transaction.lockfor root changestransaction.payment.lockfor expense payment changestransaction.lines[].lockfor individual line changes
An expense can contain both locked and unlocked lines. Keep independent line controls available, but preflight shared date, currency, type, status, listing, or payment changes because they can affect all postings.
Owner-side listing lines are rejected in an inactive ownership period. Manager-side lines can retain listing attribution when the operation permits it. Reconciliation can lock payment fields independently from financial lines.
Preview and Preflight
Create, update transaction
(PUT /transactions/{id}), remove,
batch, update transaction line
(PUT /transactions/{transactionId}/lines/{id}),
and recurring-template create/update/remove/run support dry run when the
generated OpenAPI declares the dryRun parameter. Transaction journal
regeneration intentionally does not; it needs a preview of inserts, updates,
deletes, skips, locks, and amount deltas.
Use Preview recurring transactions
(POST /transactions/recurring-templates/preview)
for occurrence dates and proposed transactions. A dry-run Run recurring
template
(POST /transactions/recurring-templates/{id}/run)
returns provisional generated transactions and rolls them back.
Mutation Recipe
- Load eligible accounts, assignments, contacts, listings, reservations, and current granular locks.
- Choose type and apply its exact shape.
- Resolve owner context and recompute party default, forced value, and options.
- Validate linked resource ownership, currency, unique reference, and totals.
- Validate payment and reconciliation records.
- Request a schedule preview for recurring templates when relevant.
- Dry-run the exact complete payload.
- Render structured lock or rule failures and discard provisional IDs.
- Confirm and apply the same payload.
- Re-read transaction, payment, lines, attachments, and locks.
Failure and Recovery
Do not preserve an invalid previous party after a dependent field changes. A value that matched the old default can move to the new default. Valid explicit overrides remain in place, and a forced rule replaces anything that conflicts.
Linked accounts, contacts, listings, reservations, bank records, and statements must belong to the selected team. Re-read after a concurrency failure. On a locked delete, offer archive only when structured context supports it.
uniqueRef identifies a transaction within its team. Every line uniqueRef
must also be unique inside that transaction; omitted line references are
assigned deterministic line1, line2, and later values. Avoid explicit
references that collide with those generated values.
Connection-scoped batch retries with the same transaction uniqueRef are
serialized. A repeated transaction or duplicate line reference is returned as
that item's structured issue rather than aborting the batch with a database
constraint error.
Payments and Reconciliation
Expense payment state lives under payment. Linked bank-record totals must
equal the transaction total. Transfer bank records must each match the transfer
magnitude. A paid expense without a payment date uses transaction date.
Vendor Bill Pay Actions
POST /transactions/{id}/pay pays a vendor bill through one of two mutually
exclusive paths selected by the required ramp flag:
ramp=false(mark as paid) records that the bill was paid outside VRTrust by settingpayment = { status: "paid", date }through the normal transaction update path, so books-closed, statement-lock, and reconciliation rules all apply. The date defaults to today.ramp=true(pay with Ramp) queues atransaction.updatedevent for the RampexpensesToBillsflow and does not change the payment status; the flow marks the bill paid asynchronously when Ramp settles. Supplying inlinevendorbank details additionally queues acontact.updatedevent for the bill's vendor contact.
Validity rules: only active expense transactions with a vendor contact can
be paid (422 otherwise); unknown ids return 404; a bill with
paidStatus=paid returns 409. Ramp additionally requires a negative
(money-out) non-zero total and exactly one active Ramp expensesToBills
listener — no listener returns 422, more than one active Ramp connection
returns 409. The endpoint supports dryRun=true; dry runs persist nothing
and queue no events.
Attachments
Upload expense attachments first with POST /transactions/attachments, then
send returned IDs in attachmentIds. Upload is an external storage operation
and is not part of mutation dry run.
On update, omit attachmentIds to keep the set, send an empty array to remove
all, or send the complete desired set. Owner visibility is stored per
attachment. Only expenses accept attachments.
Recurring Templates
Templates need a contact, expense and line configuration, payment status, start and optional end, plus daily, weekly, monthly, or yearly recurrence. Inactive templates and dates that are not due cannot run.
Common Recipes
Owner expense
Select an expense account and listing, offer both parties unless an account
rule narrows them, choose owners, set payment data if known, then dry-run the
complete expense.
POST /transactions?dryRun=true
Content-Type: application/json
{
"type": "expense",
"date": "2026-07-18",
"currency": "usd",
"description": "Pool cleaning",
"contactId": "33333333-3333-4333-8333-333333333333",
"payment": {
"status": "unpaid"
},
"lines": [
{
"description": "Pool cleaning",
"amount": 12500,
"accountId": "22222222-2222-4222-8222-222222222222",
"listingId": "11111111-1111-4111-8111-111111111111",
"party": "owners"
}
]
}Internal transfer
Select two different internal bank accounts, hide party and markup controls, enter one destination line, validate any bank-record magnitude, and preflight.
POST /transactions?dryRun=true
Content-Type: application/json
{
"type": "transfer",
"date": "2026-07-18",
"currency": "usd",
"accountId": "22222222-2222-4222-8222-222222222222",
"lines": [
{
"description": "Move funds to operating account",
"amount": 50000,
"accountId": "66666666-6666-4666-8666-666666666666"
}
]
}Statement payout
Prefer the statement pay operation. The payout it creates carries the statement reference on its line, even when the payout journal date belongs to a later statement month.
POST /statements/{statementId}/pay?dryRun=true
Content-Type: application/json
{
"ramp": false,
"date": "2026-08-08",
"bankAccountId": "66666666-6666-4666-8666-666666666666"
}Webhook Reconciliation
transaction.changed is emitted only after the externally visible aggregate
and required journal, payment, reconciliation, or attachment effects reach
their final state. It covers create, material update, archive, and deletion;
no-ops and journal-only changes that leave the public response unchanged do not
emit. The body contains only resource ID, change type, version, and href—no
type, description, amount, currency, contact, account, listing, bank,
statement, or journal detail. Recurring templates are a separate resource and
do not emit this event until they create or update a transaction.
API Reference
- Get transaction detail —
GET /transactions/{id} - Create a transaction —
POST /transactions - Update a transaction —
PUT /transactions/{id} PUT /transactions/{transactionId}/lines/{id}— ContractPOST /transactions/attachments— ContractPOST /transactions/recurring-templates/preview— ContractPOST /transactions/recurring-templates/{id}/run— Contract
