VRPlatformVRPlatform
Build a Product UI

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:

TypePurposePrimary shape
depositMoney received or matched to guest/channel activityOne or more lines
expenseCost or billContact, optional payment, markup, attachments
transferMovement between internal banksOne line plus root source account
payoutOwner or manager distributionOne 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 contextDefaultValid public options
Deposit without listing or reservationmanagermanager
Deposit with listing or reservationownersBoth unless a forced rule below applies
Opening-balance depositContext defaultBoth
ExpensemanagerBoth unless a forced rule below applies
Strict internal transferParty-neutral bank postingsHide control
PayoutownersBoth

"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 owners for deposits and expenses.
  • deposit_coHostPayout forces owners.
  • Resolved system accounts-receivable and deferred-revenue accounts force owners.
  • deposit_reserve forces manager.
  • Merchant-fee, channel-fee, and VAT deposits default to manager; an explicit owners value 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 contactId unless they are opening balances. They alone accept attachmentIds; 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.lock for root changes
  • transaction.payment.lock for expense payment changes
  • transaction.lines[].lock for 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

  1. Load eligible accounts, assignments, contacts, listings, reservations, and current granular locks.
  2. Choose type and apply its exact shape.
  3. Resolve owner context and recompute party default, forced value, and options.
  4. Validate linked resource ownership, currency, unique reference, and totals.
  5. Validate payment and reconciliation records.
  6. Request a schedule preview for recurring templates when relevant.
  7. Dry-run the exact complete payload.
  8. Render structured lock or rule failures and discard provisional IDs.
  9. Confirm and apply the same payload.
  10. 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 setting payment = { 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 a transaction.updated event for the Ramp expensesToBills flow and does not change the payment status; the flow marks the bill paid asynchronously when Ramp settles. Supplying inline vendor bank details additionally queues a contact.updated event 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

On this page