VRPlatformVRPlatform
Build a Product UI

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:

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. 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.

typeidname
userUserUser name
api_keyAPI keyAPI key label
bank_ruleBank rule, also when a user ran itBank rule name
csv_importCSV import connectionConnection name
syncConnection of the integration syncConnection name
automationNoneAutomation label
systemNoneNone

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 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 when the line has a listing or reservation. Without that owner context it defaults to manager.
  • 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.

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 chargeback line 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

  • currency must equal the team's default currency. Create, update, and batch writes reject any other value with reason: "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 contactId unless they are opening balances. They alone accept attachmentIds; their lines can carry markup and tax application.
  • Expense markup requires an owners line. A line without party counts as manager and rejects markup.
  • Applied-amount tax is always additive (excluded). Send only appliedAmount.taxRateId; applied-amount tax has no public behavior setting. Markup tax remains separate and may specify included or excluded behavior. With included, 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. With excluded or 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 line party accepts only owners or manager; 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 contactId and no accountId uses that contact's default account.
  • matchBankAccountLast4 applies only when the root accountId is 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 external banking category is stored inactive. Linking a BankRecord from an external account also stores a non-expense transaction inactive.

Editability

Transaction reads expose:

  • transaction.lock for root changes
  • transaction.payment.lock for expense payment changes
  • transaction.lines[].lock for 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 lockEditability
Transaction and line descriptionsEditable
Line account and system assignmentEditable
Line party (the form's bill-to attribution)Editable
Line listing and reservationEditable
Expense line markupEditable
Line amountLocked; unmatch first
Transaction dateLocked; unmatch first
Root bank or credit-card accountLocked; unmatch first
Add or remove linesLocked; unmatch first
Other transaction or line fieldsLocked 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.

  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, 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: null records an offline payment from accountId. An expense paid outside a trust account omits accountId: its payment has no account and clears A/P against the expense reimbursement account. Payouts require accountId. An offline payment settles and allocates immediately, and its cash legs post on requestedDate. A requestedDate in 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 transaction underpaid; the next payment defaults to the rest. Other amount above remainingAmount fails with insufficientBalance.
  • provider: "ramp" submits an ACH payment through fundingAccountConnectionId, 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 sets amountMismatch and 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 includes operation for polling. Only one open Ramp payment is allowed per transaction; a second request fails with providerPaymentOpen.
  • An offline payment on a payout fails with providerPaymentOpen while 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 providerPaymentOpen unless cancelOpenPayments: 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.
  • replacesPaymentId creates a replacement for a provider payment. The backend requests cancellation of the previous payment. The replacement stays pending and 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:

ActionUnavailable when
archiveThe transaction is archived, a Ramp payment is open (paymentOpen), or a payout is in a locked period or statement
unarchiveThe transaction is active (transactionActive)
deleteA Ramp payment is open (paymentOpen), any Ramp payment exists (providerHistory), or the period or statement is locked
markUnpaidOnly 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
recordReturnOnly payouts with a settled offline payment (notPaid otherwise), and not after a return (returned)
editFinancialsA Ramp payment is open (paymentOpen), or the period or statement is locked
recordOfflineNothing 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

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.

On this page