Transactions
Build transaction forms with valid types, party, payment, and locks
Last Updated: 2026-10-18
Version: 1.7
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. A template is not a realized transaction and has no journal entries of its own; related listing, account, and tax-rate changes refresh only generated transactions.
Read Model
Render the returned transaction, payment, balance, 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.
GET /transactions search uses case-insensitive substring matching across the
transaction description, unique and short references, root and line contact
first, last/company, and full names, line descriptions, and linked reservation
confirmation number, unique reference, PMS reference, and guest name. Other
filters, including transaction type, combine with search.
GET /transactions returns the newest transaction date first. Use sort with
date, amount or createdAt, and prefix - for descending order. The
default is -date. Ties fall back to the transaction ID in the same direction.
The server sorts the complete filtered set before pagination. Any other value
returns 400.
Every transaction response carries createdBy, the source of the creation
event in the audit log: type (user, api_key, sync, automation,
system, bank_rule or csv_import), id, and name. It has no email.
type | id | name |
|---|---|---|
user | User | User name |
api_key | API key | API key label |
bank_rule | Bank rule, also when a user ran it | Bank rule name |
csv_import | CSV import connection | Connection name |
sync | Connection of the integration sync | Connection name |
automation | None | Automation label |
system | None | None |
createdBy is null when no bank rule application, CSV connection or
creation event in the audit log identifies the source. That covers most
transactions created before audit recording started in their region, 26 Feb
2026 at the earliest. An integration sync shows as sync only when the
transaction belongs to a connection; otherwise it is system. A CSV upload
shows the CSV connection, not the upload or the person who uploaded it. After a
bank rule is deleted, its transactions show the user or system that ran it.
A partner API key creating on behalf of an end user shows as the API key, not
the end user.
appId filters by the app of the connection returned on the transaction,
such as csvImport, and excludes manual transactions, which have no connection.
A CSV file imported through another app's connection, such as Booking.com or
Guesty payouts, keeps that app. connectionId=null returns manual transactions.
hasRecurringTemplate=true returns transactions generated from any recurring
template; recurringTemplateId selects one template.
bankRule names the BankRule that created the transaction while the
transaction stays matched to the BankRecord the rule ran on, and
matchedByBankRule is then true. BankRules only create transactions; they
never match an existing one, so a manually entered transaction returns
bankRule: null. Download the CSV file a transaction was imported from with
GET /sources/{id}/download?type=transaction, passing the transaction ID. The
response names the original file.
Recurring-template lists filter the complete result set before pagination.
Use contactId for the root vendor/contact, accountId for the root payment
account or any line account, listingIds with
listingScope=exact|withChildren, frequency, dateRange with
isDateRangeEndInclusive, and signed-cent amount comparisons or ranges.
Omitted listing scope includes direct child listings.
Date ranges match schedule active-period overlap: endDate is inclusive,
count ends the period on its final recurrence, and a schedule without either
boundary remains open-ended. Search matches the template description and
reference, root and line account/category names, and line listing names and
references. All supplied filters combine with AND semantics.
Owner Payout History
Use List contact payouts
(GET /contacts/{contactId}/payouts) for an Owner Portal
payment history. The endpoint is paginated across statement periods and
returns active payout transactions newest first.
Owners may request only contact IDs in their server-derived owner access. A different contact returns an opaque not-found response. Property managers and owner-preview sessions may request a selected contact in the current team.
Each row contains the payout identity, date, description, signed cent amount,
currency, optional statement reference, the payout balance, and its
payments. A payment carries its provider, method, status, amounts, the masked
destination account suffix captured when it was created, and its settlements.
Bank details, reconciliation, journals, connection IDs, and provider routing
references are absent.
A statement reference includes its month and listing: { id, name }, so payment
history can identify the property without exposing journal lines. A payout
linked to more than one statement returns ownerStatement: null rather than
selecting a potentially misleading listing.
Transaction CSV
GET /transactions/csv and POST /transactions/csv accept the same sort as
GET /transactions, so an export keeps the order of the table it came from.
When GET /transactions/csv or POST /transactions/csv is filtered with
type=payout, each exported row includes the payout line's listing name in a
Listing column. A payout without listing attribution leaves the value blank.
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_coHostPayoutforcesownerswhen the line has a listing or reservation. Without that owner context it defaults tomanager.- 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.
Security Deposit Accounting
The PMS reservation financial line is the source of refundable guest-deposit principal. Map that line to Security Deposits Payable. It creates the guest receivable and equal liability independently of where the channel sends the cash.
For example, when Hostaway reports 2,000 USD rent plus a 500 USD refundable deposit but remits only 2,000 USD, the settlement Deposit clears 2,500 USD of reservation receivable, posts 2,000 USD to the bank, and posts 500 USD to Reserved Funds withheld by Merchant Processor. The settlement reconciles to the 2,000 USD BankRecord. The retained 500 USD is a processor receivable, not trust cash, until remitted.
deposit_reserve posts that processor receivable and forces the manager
party. It is not the guest liability and does not prove that every processor
reserve is security-deposit principal. Remittance moves the asset from the
processor receivable to the trust bank without releasing the liability.
Refund or forfeit the deposit with open-period
reservation adjustments.
A full refund reverses the liability; a forfeiture reverses it and reclassifies
the same value to the explicitly selected revenue account and owners or
manager party. The cash refund or processor release remains a Transaction
against the asset that moved. There is no beneficiary default, top-level
security-deposit resource, or persisted lifecycle chain.
These rules cover a chargeback of refundable deposit principal.
Base Reservation Chargebacks
For a chargeback against rent or another base reservation charge, record and reconcile the cash withdrawal as a refund Transaction. Keep the disputed amount in accounts receivable while the outcome is unresolved. Record any processor chargeback fee separately as a bank-fee expense.
If the processor returns the cash, record the incoming cash against accounts receivable. Do not change the reservation.
If the chargeback is lost, use negative owner-party reservation adjustment entries on an explicit valid posting date for the amount being written off. The amount can cover part or all of the reservation. Choose the treatment that matches the team's accounting policy:
- Keep the original rent and cleaning amounts and post a separate offset to a
selected Chargeback expense or contra-revenue account. A team can map a
custom
chargebackline type to that account. - Reduce the original revenue categories with adjustments mapped to the affected rent, cleaning, or other accounts.
The Chargeback account and line mapping are optional team configuration.
chargeback is not a reserved system line type. This example writes off
1,250 USD using the separate-offset treatment:
POST /reservations/{reservationId}/adjustments?dryRun=true
Content-Type: application/json
{
"type": "chargeback",
"amount": -125000,
"party": "owners",
"description": "Chargeback lost",
"generalLedgerPostingAt": "2026-09-04"
}The adjustment reduces the reservation financials and clears the corresponding
receivable in the open period. Apply the same request without dryRun=true
after reviewing the returned financials and posting date. It does not rewrite
the original payout, published statement, or closed month.
Review the original management commission, cleaning fees, and other fees separately. Decide which remain earned and which must be reduced or reversed. A separate Chargeback offset leaves the original rent and cleaning fee bases intact. Adjusting those original accounts can produce fee corrections, but the result depends on each fee's formula, conditions, and locks. Check the fee calculation rules and preview the resulting financials; do not assume all original fees reverse.
For example, 1,000 USD rent plus 250 USD cleaning, less 200 USD commission and 250 USD cleaning fees to the manager, leaves 800 USD for the owner. An owner-borne 1,250 USD Chargeback offset leaves the owner at -450 USD. If both manager fees must also be refunded, corrections totaling 450 USD bring the owner to zero. If those fees remain earned, zero is not the intended result.
The owner-party adjustment assigns the loss to the owner. To assign some or all
of it to the manager, configure a recurring fee whose formula reads the
Chargeback account. Use owners on the fee debit and manager on the fee
credit. A 100% rate transfers the full adjustment to the manager; a lower rate
splits it. This fee allocates the chargeback loss; it does not reverse the
original commissions or cleaning fees. The fee must accept negative Chargeback
totals because a positive-only account condition excludes the adjustment.
Preview the fee against a representative reservation before assigning its
listing period.
This mapping and fee are team accounting policy. VRPlatform does not select the loss recipient or apply the adjustment while the dispute is unresolved. Owner-statement grouping and visibility are presentation choices. Hiding a chargeback line does not remove its effect on the owner's balance.
Type Constraints
currencymust equal the team's default currency. Create, update, and batch writes reject any other value withreason: "currencyMismatch"; in a batch, the rejection is that item's issue.- 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. - Expense markup requires an
ownersline. A line withoutpartycounts asmanagerand rejects markup. - Applied-amount tax is always additive (
excluded). Send onlyappliedAmount.taxRateId; applied-amount tax has no public behavior setting. Markup tax remains separate and may specify included or excluded behavior. Withincluded, the entered markup amount contains the tax: at 20%, a markup of 1.20 posts 1.00 of markup and 0.20 of markup tax. Withexcludedor no behavior, the entered amount posts as markup and the tax is added on top. - Applied amount and markup tax postings remain balanced when the configured tax account forces the owner party; no account-category workaround is required.
- Recurring expense templates require a root
contactId. Template linepartyaccepts onlyownersormanager; other values are rejected before the template is written. - 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 and reject markup. Statement linkage
belongs on the line as
ownerStatementId.
Account Resolution
- A line with
contactIdand noaccountIduses that contact's default account. matchBankAccountLast4applies only when the rootaccountIdis omitted. The API selects the active account with those last four digits, then an inactive account whose name contains them.- Setting or changing a bank account's last four digits remaps unlocked deposits that are not opening balances. Deposits on that account hinted with the old digits lose the account; deposits without an account hinted with the new digits receive it.
- A transaction whose root account is a bank account with the
externalbanking category is stored inactive. Linking a BankRecord from anexternalaccount also stores a non-expense transaction inactive.
Editability
Transaction reads expose:
transaction.lockfor root changestransaction.payment.lockfor expense payment changestransaction.lines[].lockfor individual line changes
Changing a locked expense payment returns 403 FORBIDDEN. Its message identifies
closed books or an attached published statement; context.lockReason is period
or statement. A books-closing denial also includes context.booksClosedAt, the
first open date. Moving the payment date forward does not unlock its existing postings.
An update that changes only payment and optionally the root accountId is
allowed while the expense date is in closed books or its expense postings are
locked. It is still rejected when the payment postings are attached to a
statement or in closed books, or when the new payment date is before
booksClosedAt.
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.
The recurring-expense scheduler skips an occurrence when an owner-side line falls inside an inactive ownership period. It creates no transaction for that date. Interactive transaction creates and recurring-template runs still return the inactive-listing rejection so the request can be corrected.
The scheduler also skips an occurrence that falls in a closed period or under a
published statement and creates no transaction. The effect completes instead of
failing, and the template lists the date under missingInstances when it is
within the last 12 months. Each night
the scheduler tries every past date again, so the instance is created once the
lock is lifted. Other rejections, such as an invalid line party, keep the effect
failed.
For deposits and expenses, a bank-record match is a field-level constraint.
The aggregate transaction.lock.status remains locked and
transaction.lock.reconciled.bankRecordIds identifies the reason. Do not use
the aggregate status alone to disable the complete edit form.
| Bank match is the only lock | Editability |
|---|---|
| Transaction and line descriptions | Editable |
| Line account and system assignment | Editable |
| Line party (the form's bill-to attribution) | Editable |
| Line listing and reservation | Editable |
| Expense line markup | Editable |
| Line amount | Locked; unmatch first |
| Transaction date | Locked; unmatch first |
| Root bank or credit-card account | Locked; unmatch first |
| Add or remove lines | Locked; unmatch first |
| Other transaction or line fields | Locked unless another documented flow allows them |
The rule applies to each touched line. A books-closed period, attached owner statement, or saved statement period still wins and returns its normal lock reason. Transfers are outside this field-level exception. Saving the editable fields preserves the bank-record link.
Changing a deposit line's reservation moves it between reservation and non-reservation items. A bank match alone does not block either direction, and an owner-statement lock on another line does not block the move. Preflight both the source and destination listing periods: the API rejects the move if it would pull an owner posting into or out of a saved statement period.
When update transaction line changes reservationId, other
lines of the same transaction whose uniqueRef starts with the edited line's
uniqueRef move to the same reservation. Their party follows the same default
transition.
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.
Generated-instance creation and template deletion serialize on the template. If creation starts first, deletion waits, keeps the generated expense, and clears its template link. If deletion finishes first, a queued occurrence ends without creating an expense.
Mutation Recipe
For a deposit line edit, send only lines_update to
PUT /transactions/{id}. Unlisted source lines and their journal postings stay
unchanged, including historical party attribution. The selected lines and shared
bank postings are validated before success. Books-closed, statement-attachment,
and source or destination P&L period locks still apply.
- 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, connections, 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.
DELETE /transactions/{id}?onLocked=archive never archives transfers or
payouts; a locked transfer or payout returns its lock error. Archiving another
locked type sets status to inactive, clears uniqueRef, removes its
BankRecord links, and leaves every journal entry unchanged.
Background transaction journal refreshes preserve statement-attached rows. If the stored journal and complete desired journal are each balanced but applying only the unlocked changes would make the stored journal unbalanced, the refresh finishes as locked and leaves every journal row unchanged.
For POST /transactions/journal-entries, exactly one explicit ID completes
before success. Multiple IDs or an omitted selection queue a
transaction-journal-refresh operation. Follow the returned operation until
it is terminal; an empty ID collection is invalid.
If two requests delete the same transaction concurrently, one removes it and
the other returns the standard NOT_FOUND response. Treat that response as an
already-removed resource and re-read the collection.
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.
When lines replaces a transaction's lines, a line matches an existing line by id, or, when it
has no id, by its uniqueRef. A matched line is updated in place and keeps its id. A field
the sent line omits keeps its stored value, and null clears it. An existing line that no sent
line matches is deleted. A line with payment history cannot be deleted.
Creating or updating a transaction with a uniqueRef already used by another
transaction returns a structured user error without changing either record.
An empty string is a stored reference and follows the same uniqueness rule;
use null or omit the field when the transaction has no reference.
Connection-scoped transaction batches resolve an existing row by explicit
id, then uniqueRef, then sourceId. Keep both external references stable
when they are available. Concurrent retries sharing either reference are
serialized. A repeated transaction, repeated sourceId inside one batch, or
duplicate line reference is returned as that item's structured issue rather
than aborting the batch with a database constraint error.
An item returned as an issue changes nothing: its transaction, line and audit
writes are rolled back, and the other items still apply.
Payments
Expense and payout details show the total paid and remaining balance when a transaction already has partial or multiple payments. The UI does not offer an amount editor for new partial payments. ACH dialogs show the available balance they submit. "Recreate payment" cancels the open Ramp payment and requests its replacement on the same payout.
A payment is one cash movement against one obligation. Its settled date,
posting date, line allocations and journal entries belong to the payment; a
reversal undoes the whole payment. GET /transactions/{id}/payments returns
the obligation balance (payableAmount, netSettledAmount, pendingAmount,
remainingAmount, availableAmount, overpaidAmount,
reconciliationRequired), server-owned actions (pay, recordOffline, archive,
unarchive, delete, markUnpaid, recordReturn, editFinancials), and
every payment with settledAt, postingDate, allocations, reversal,
providerBillRef, providerPaymentRef, and per-payment actions (cancel,
replace, reverse). Provider payments also carry the provider's latest
providerStatus, providerMethod, providerReference, scheduledAt, initiatedAt, and the
providerFailureCode of the latest rejected command. providerReference is the provider's
customer-facing payment ID (Ramp's PAY#); it stays null until Ramp assigns one. Each action carries available and a finite
reason when unavailable. GET /transactions/{id} includes the same balance;
GET /transactions items include balance, payments, and actions.
transactions:mark-paid:reimbursement is a narrower payment scope granted to
Operations. It records a payment that names no funding account. POST /transactions, PUT /transactions/{id}, POST /transactions/{id}/payments with
provider: null, and recurring template create and update accept it only when
the stored and resulting accountId are null and no bank records are attached,
stored or requested. For recurring templates only the resulting accountId
must be null. An omitted accountId on transaction update keeps the stored account.
An offline payment created without accountId records a reimbursement. A payment that
names any account, trust or operating, or that attaches bank records, returns
403 with reason paymentAccountRequiresMarkPaid. So does moving a paid
expense to or from an account, or marking a bank-paid expense unpaid. Batch
upserts and Ramp payments require transactions:mark-paid. Recurring template
create or update with paymentStatus: "paid" requires a payment scope; the
instances copy the template account.
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.
POST /transactions/{id}/payments accepts the reimbursement scope for an
account-less expense under the rules above. Other payments require
transactions:mark-paid; payouts
additionally require statements:record-payout, and provider: "ramp"
additionally requires ach-payments:execute. The body names the method, an
optional amount (provider: "ramp" defaults to the available balance; offline
payments default to the remaining balance), requestedDate, reference, and a
stable idempotencyKey. Replays with the same key return the existing payment;
different instructions under the same key return 409 CONFLICT.
provider: nullrecords an offline payment fromaccountId. An expense paid outside a trust account omitsaccountId: its payment has no account and clears A/P against the expense reimbursement account. Payouts requireaccountId. An offline payment settles and allocates immediately, and its cash legs post onrequestedDate. ArequestedDatein closed books is refused with the books-closing error and nothing is saved. Inside a published owner statement period the normal statement lock applies: expense payment legs post on their date, payout legs are refused. Partial amounts leave the transactionunderpaid; the next payment defaults to the rest. Other amount aboveremainingAmountfails withinsufficientBalance.provider: "ramp"submits an ACH payment throughfundingAccountConnectionId, the Ramp account connection of the funding bank account. One Ramp bill is created per payment when the user pays, and its bill lines equal the payment's nonzero allocations. Unallocated transaction lines do not require Ramp account mappings. The payment reserves its amount (pendingAmount) until Ramp confirms cash equal to the payment amount; a difference setsamountMismatchand posts nothing. A Ramp settlement posts on its settlement date, moved forward to the first open date when that date is locked, because the cash already moved. The response includesoperationfor polling. Only one open Ramp payment is allowed per transaction; a second request fails withproviderPaymentOpen.- An offline payment on a payout fails with
providerPaymentOpenwhile a Ramp payment is open; cancel the Ramp payment first. - An offline payment on an expense above the available balance while a Ramp payment is open
also fails with
providerPaymentOpenunlesscancelOpenPayments: true, which records the offline payment and asks the provider to cancel the payments still reserving funds. This includes failed or canceled executions whose bill remains open or whose funding debit has not been refunded. - Owner payouts paid through Ramp are cash basis. Once a payout has a Ramp payment, it posts nothing until a payment settles; each settled payment then posts the whole payout journal (Owner Payouts, Accounts Payable, cash) for its amount. A failed or canceled Ramp payment leaves nothing in the books. A payout without a Ramp payment posts its distribution on the payout date, as before.
replacesPaymentIdcreates a replacement for a provider payment. The backend requests cancellation of the previous payment. The replacement stayspendingand submits automatically once the predecessor has closed and refunded its funding debit. If the predecessor settles first or the remaining balance no longer covers the replacement, the unsubmitted replacement is canceled.
The payment object on POST /transactions and PUT /transactions/{id} keeps
working for offline bookkeeping: status: "paid" records an offline payment for
the open balance on payment.date from the transaction's accountId, or as a
reimbursement when an expense has none, and status: "unpaid" undoes the offline
payments: their journal entries are removed, and the payments stay in history with a
reversal of kind offlineUndo. A changed payment date or account undoes the offline
payment the same way and records a new one. Marking the transaction paid again records a new
payment. Provider-settled transactions cannot
be marked unpaid. Matching a bank record to an unpaid bill records the offline
payment on the bank record's date and account. Matching a bank record to a transaction
already paid offline on the same date and account links that payment and posts nothing
new. Unmatching removes the link only; the transaction stays paid.
Payment statuses are pending, submitting, scheduled, processing,
succeeded, failed, and canceled. For Ramp, scheduled means Ramp accepted the bill
but has not initiated the transfer (approval pending, payment ready, approved, scheduled);
a payment unscheduled in Ramp stays scheduled with scheduledAt empty;
processing means Ramp initiated the transfer or it is in transit. A bill Ramp has not
accepted yet leaves the payment pending or submitting. Screens label them Requested,
Scheduled (Awaiting approval while Ramp waits for approval, Unscheduled without a
scheduledAt), Processing, Completed, Failed,
Canceled and Returned. A transaction without a payment reads Not sent, and one settled by an
offline payment reads Paid offline. scheduledAt and initiatedAt are calendar dates in UTC.
The requested arrival date applies only while the payment is open.
Ramp sends no webhook when a scheduled payment is unscheduled or moves from approval to scheduled, so VRPlatform reads every open Ramp payment again every five minutes. A change made in Ramp shows within about five minutes.
A rejected Ramp submission sets providerFailureCode to a short static code, for example
categoryAccountMissing, vendorBankAccountMissing, listingMissing, routingIncomplete,
vendorContactMissing, vendorSetupIncomplete, fundingAccountInvalid,
billAmountMismatch or allocationInvalid. Clients show their own message for a code and a
generic one for a code they do not know. The accounts or refs behind a code appear only in
the Trigger.dev run logs, never in the code. A payment that stays submitting for 30
minutes gets the unknownOutcome issue from a scheduled sweep. A payment can additionally carry an
issue (for example unknownOutcome, currencyMismatch, unlinkedMovement,
amountMismatch, accountingConfiguration) that sets
balance.reconciliationRequired and blocks new payments until it is resolved. A
settlement in another currency does not contribute to the transaction's paid status or
settled balance. A payment has posted when its journal entries carry its ID; a configuration
failure records accountingConfiguration and retries posting, including a reversal whose
original payment already posted. An offline reversal posts on the supplied posting date; a
date in closed books is refused. A provider return moves forward to the first open date when
locked. Repeating the same reversal instructions and idempotency key returns the existing
reversal. Changed dates or reasons conflict.
POST /payments/{id}/cancel asks the provider to cancel an open payment or to
close its unpaid bill and returns the operation that tracks it; the payment
stays open until the provider confirms.
POST /payments/{id}/reverse reverses the whole settled expense payment on its own
date, for example a bounced check, and reopens the balance. A payout payment is
refused with payoutReturnRequired. Provider returns
arrive from the provider and cannot be reversed by hand.
POST /transactions/{id}/returns records money returned from an owner payout
paid offline, for example a bounced check. It creates an owner receipt: a positive
payout with its own reference, linked to the original through
returnedFromTransactionId, with the original line's statement, posted on
postingDate with the payout journal reversed. The original payout stays paid and
reads the receipt as returnTransactionId; the owner is owed the amount again on
the statement. A Ramp return of a settled payout creates the same receipt. One
active return exists per returned payment; repeating the request returns it.
The route requires transactions:mark-paid, statements:record-payout, and
transactions:post-journals. Rejected cancellation
commands clear the pending request so a new cancellation can be submitted.
Action availability also reflects the caller's payment, journal, ACH, and
owner-payout permissions.
Every refused payment mutation returns 409 CONFLICT with context.reason from
one finite set: transactionInactive, transactionNotPayable,
insufficientBalance, reconciliationRequired, paymentTerminal,
paymentOpen, providerPaymentOpen, paymentReplaced, predecessorOpen,
connectionUnavailable, providerFlowInactive, providerSubmissionsPaused,
fundingAccountUnavailable, recipientUnavailable, accountUnavailable,
offlinePayment, providerPayment, fullyReversed, payoutReturnRequired, historicalCopy,
generalLedgerUnavailable, providerHistory, providerSettled, periodLocked,
statementLocked, transactionActive, notPaid, and returned. The same values appear as
disabled reasons in actions.
Transaction actions follow one rule set. Only an open Ramp payment locks a transaction:
| Action | Unavailable when |
|---|---|
archive | The transaction is archived, a Ramp payment is open (paymentOpen), or a payout is in a locked period or statement |
unarchive | The transaction is active (transactionActive) |
delete | A Ramp payment is open (paymentOpen), any Ramp payment exists (providerHistory), or the period or statement is locked |
markUnpaid | Only expenses and payouts with Ramp history (transactionNotPayable otherwise). A Ramp payment is open (paymentOpen) or settled (providerSettled), nothing is paid offline (notPaid), or the payment is locked |
recordReturn | Only payouts with a settled offline payment (notPaid otherwise), and not after a return (returned) |
editFinancials | A Ramp payment is open (paymentOpen), or the period or statement is locked |
recordOffline | Nothing remains to pay (insufficientBalance), or a payout has an open Ramp payment (providerPaymentOpen). On an expense with an open Ramp payment, an amount above the available balance needs cancelOpenPayments: true |
A failed, canceled or succeeded Ramp payment no longer blocks archive, unarchive, edits or Mark
as unpaid. Archive and unarchive only change accounting; they never touch Ramp. Archiving an
item that Ramp already paid does not reverse the payment. A payout whose Ramp payments all
failed or were canceled has nothing in the books, so Mark as paid records an offline payment on
that same payout. Editing such a payout, or archiving it, records no payment. Mark as unpaid
on an expense, or on a payout paid offline after Ramp history, undoes its offline payment and
removes the payment's journal entries; the payment stays in the payment history. A payout
created offline from a statement is paid by nature and is archived instead. A Ramp-settled expense
cannot be marked unpaid. An expense's vendor and Bill ID stay bound to the Ramp bill once one
exists and return providerBill when changed.
Linked bank-record totals must equal the transaction total. Transfer bank
records must each match the transfer magnitude. Ramp payment state does not
lock expense date, descriptions, attachments, or line coding; amount, tax,
markup, line-count, and payment changes are refused with paymentOpen while a provider
payment is open, and archiving an expense or payout is refused the same way. Deleting a transaction also deletes its offline payments and their
journal entries. A transaction with provider payment history (providerHistory), and a
transaction line with payment allocations, cannot be deleted; provider evidence
remains available for audit.
Attachments
Create an expense attachment upload with POST /transactions/attachments and
send filename, contentType, fileSize, and isOwnerAccessible. The
response contains attachmentId, a 24-hour opaque uploadRef, its
expiresAt, and an authenticated regional uploadUrl. PUT the original file
to uploadUrl with the declared Content-Type, then send attachmentId in
attachmentIds. The request must arrive with a Content-Length matching
fileSize; browsers provide it when the original File is the request body.
Files must be non-empty and at most 100,000,000 bytes. Upload is an external
storage operation and is not part of mutation dry run.
The browser must pass the original File as the PUT body. Do not read it into
an ArrayBuffer, construct a multipart form, decode uploadRef, or send a
storage completion request. The staged attachment's 24-hour orphan window
starts before storage begins. Failed uploads remain unavailable and are
eligible for cleanup. Retrying the same completed upload returns success
without overwriting the stored object. JSON media types are streamed unchanged.
Deleting an attachment also invalidates retries through its still-live upload
reference.
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 cannot run.
Template lines preserve the applied-amount tax rate and markup configuration. Every generated expense receives those settings; applied-amount tax remains additive while markup retains its configured tax behavior.
A run without dates queues every scheduled occurrence from the start date
through today. Explicit dates skip the schedule check, so each listed date
creates an occurrence. An occurrence whose generated expense already exists
returns that expense unchanged. If the template is deleted
before a queued occurrence starts, that stale occurrence completes without
creating a transaction. A direct run request for an unknown template still
returns NOT_FOUND.
Deleting a template waits for generated-transaction writes already in flight. Generated transactions that commit first remain as independent expenses with their template link detached; the template delete then completes normally.
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 payouts it creates carry the statement reference on their lines, even when the payout journal date belongs to a later statement month.
POST /statements/pay?dryRun=true
Content-Type: application/json
{
"date": "2026-08-08",
"statements": [
{
"statementId": "55555555-5555-4555-8555-555555555555",
"payouts": [
{
"contactId": "77777777-7777-4777-8777-777777777777",
"provider": null,
"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
- Create a transaction —
POST /transactions - Update a transaction —
PUT /transactions/{id} PUT /transactions/{transactionId}/lines/{id}— ContractPOST /transactions/attachments— ContractPUT /transactions/attachments/uploads/{uploadRef}— ContractPOST /transactions/recurring-templates/preview— ContractPOST /transactions/recurring-templates/{id}/run— Contract
Related
During a staged payments rollout, transactions paid before payment records existed keep their paid state and cash journals. Earlier API deployments reject changes to transactions that already have payment records. Use the deployment with payments support to update those transactions. Owner statement status changes still work on earlier deployments: they only attach or detach the journal entries of the statement period and never change payment evidence. Provider settlement and return evidence in another currency requires reconciliation and does not post or reverse cash. Provider-origin payment allocations appear in public audit history.
Older provider success events and contradictory status events with the same provider timestamp
are retained as evidence but cannot mark a payment paid. An observation without an execution
status does not block settlement evidence with the same timestamp. Payment-only transaction
updates emit transaction.changed after settlement changes.
