Transactions
Build transaction forms with valid types, party, payment, and locks
Last Updated: 2026-09-05
Version: 1.5
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, latest providerPayment, 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.
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. A payout appears as soon as
its latest provider payment is requested; no provider status is hidden.
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 only the payout identity, date, description, signed cent
amount, currency, optional statement reference, and latest owner-facing
provider payment. The provider payment includes the masked destination account
suffix captured for that attempt and the requested arrival date when available.
Later ACH changes do not alter an earlier payout's destination. A missing
historical snapshot returns destinationAccountLast4: null.
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.
A recorded payout without provider dispatch returns providerPayment: null.
Full bank details, reconciliation, journals, connection IDs, provider
references, sync IDs, and attempt metadata are intentionally absent.
Returned Owner Payouts
Record a full or partial external owner-payout return with
POST /transactions/{id}/payment-returns. The original payout stays active
and keeps its payment history. The operation creates a positive compensating
payout through the normal transaction and journal path and links it with
returnedFromTransactionId.
The operation requires transactions:read, transactions:payouts:write, and
transactions:mark-paid because it reads the original payout and records its
compensation as paid.
The body requires a stable idempotencyKey, positive cent amount,
timezone-qualified effectiveAt, and explicit open-period postingDate. The
API never moves a return out of a closed period. An optional incoming
bankRecordId is reconciled to the compensation atomically; it must belong to
the original bank account and exactly match the returned amount.
Version one accepts an active, paid, one-line owner payout linked to an owner statement. It rejects manager payouts, inactive payouts, return compensations, ambiguous allocations, and cumulative amounts above the original payout. The effective time cannot precede the original payment date. Reusing an idempotency key with the same input returns the existing compensation; changing the input returns a conflict with the differing field names.
After the first compensation, the original and every compensation are accounting facts: they cannot be archived, deleted, or financially edited. Description-only and bank-record reconciliation changes remain available.
Outgoing payout reads expose returnSummary with none, partial, or full
status, cumulative and remaining amounts, active compensation IDs, and the
direct replacement ID. A compensation exposes returnedFromTransactionId and
returnEffectiveAt; it does not have its own return summary. A replacement
continues to use recreatedFromTransactionId and may not exceed the amount
already returned.
Ramp returned remains provider evidence, not an accounting write. The
current stored Ramp evidence has no distinct returned amount or effective
timestamp, so VRPlatform does not manufacture either value or automatically
post compensation. Use the provider-neutral operation with verified evidence.
Archiving a returned Ramp bill leaves payout accounting active. Failed and
canceled recovery continues to archive payout accounting.
Transaction CSV
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
- 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. - 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. - 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. Statement linkage belongs on the line as
ownerStatementId.
Editability
Transaction reads expose:
transaction.lockfor root changestransaction.payment.lockfor expense payment changestransaction.lines[].lockfor individual line changes
An expense can contain both locked and unlocked lines. Keep independent line controls available, but preflight shared date, currency, type, status, listing, or payment changes because they can affect all postings.
Owner-side listing lines are rejected in an inactive ownership period. Manager-side lines can retain listing attribution when the operation permits it.
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.
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.
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
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. 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.
Creating or updating a transaction with a uniqueRef already used by another
transaction returns a structured user error without changing either record.
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.
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.
Provider Payments
POST /transactions/{id}/pay requires transactions:mark-paid for both offline
recording and ACH requests. Ramp execution additionally
requires ach-payments:execute, including when called after saving the expense
form or from a bulk payment action. Cancel uses
POST /transactions/{id}/provider-payments/{providerPaymentId}/cancel and
requires both permissions. These checks follow the team's
authorization mode.
Creating or updating an expense, including a batch payment-field update, records
accounting data only. A Ramp-linked account alone does not request ACH;
execution requires the explicit provider: "ramp" payment call.
Choose the provider explicitly:
provider=nullrecords an active expense as paid outside Ramp through the normal transaction update path. Books-closed, statement, and reconciliation locks still apply. Pass the selected GL bank account asfundingAccountId. The payment date defaults to today. After a terminal expense ACH attempt and permanent Ramp bill archive, this endpoint can record an offline payment, including after unarchiving the expense. It preserves the archived bill and payment history without scheduling another Ramp payment.provider="ramp"requests ACH execution for an active vendor expense or an existing owner payout. Expenses dispatchtransaction.updatedtoexpensesToBills; payouts dispatch their linkedownerStatement.updatedtotransfersToBills. An expense becomes paid when the request is accepted, with an accounts-payable debit and cash credit on the selected account. A reconciliation effect follows each expense request. If dispatch fails, it resets the expense to Unpaid and refreshes the journal so the payment entries no longer remain active.
Ramp execution requires exactly one eligible matching Ramp flow, a recipient
contact with paymentMethods.ach.status=ready, and a ready selected funding
account identified by fundingAccountId. A matching flow with no status is
unconfigured and eligible; the first otherwise-valid payment activates it.
Dry runs and failed validation leave the status absent. An explicitly inactive
flow remains unavailable, and multiple eligible connections return 409. The
selected AccountConnection must resolve to one imported Ramp entity and source
bank account; those exact provider IDs are pinned to the dispatch instead of
choosing a default Ramp entity or account. A request returns syncId plus the
latest provider attempt. Reusing an idempotencyKey returns the same request
only when its transaction and funding account match; using it for a different
payment returns 409.
The selected funding AccountConnection must link to a GL bank Account. Before
the provider attempt is recorded, the API moves the expense or payout root
accountId to that linked Account and refreshes its journal cash leg through
the normal locked transaction path. A books-closed lock still rejects the
change and no Ramp request is created.
Each outbound Ramp bill line uses the account assigned to that VRPlatform
transaction line as its Ramp Category. Payout and payout-style transfer lines
without an explicit account use the team's payout_distribution account,
matching the journal engine. These accounts must exist in Ramp through the
VRPlatform chart-of-accounts push flow. Before dispatch, the API requires a
completed pushRampAccounts run that started after each used account was
created or last changed. A full run covers the complete chart; an account event
covers only that exact account. Missing evidence returns 422 with Ramp Category account is not ready and creates no provider-payment request. Run the
Ramp chart-of-accounts flow and retry the payment.
Transaction reads expose the latest attempt as providerPayment with
requested | processing | paid | failed | returned | canceled, provider
status detail, payment reference and method, requested arrival target,
masked destination account snapshot, scheduled and actual initiation
timestamps, completion timestamp, Transfer API return evidence, Ramp bill
archive timestamp, failure reason, and stable attempt ID. The requested arrival
is the date submitted to Ramp; Ramp does not return a revised estimated-arrival
field. Use
GET /transactions/{id}/provider-payments for complete attempt history.
Transaction reads also expose providerPaymentActions. This server-owned
matrix covers pay, cancel, cancelAndArchive, cancelAndRecreate,
archive, archiveAndRecreate, and unarchive. Each action contains
available plus a static disabled reason. Clients must still enforce their
permission model, and every mutation revalidates state after the read.
For expenses, Cancel deschedules the payment while preserving the Ramp bill.
Archive permanently archives the bill. Failed requests that never created a
Ramp bill can still be archived locally.
When the latest request failed or was canceled before its Ramp result was recorded, Cancel remains unavailable. Its disabled reason describes that terminal request instead of asking the client to wait for reconciliation. Read the action matrix for available Pay and Archive actions; earlier bill history does not make the latest request cancelable.
A payout can be submitted from VRPlatform only once. Ramp users may retry its
failed payment in Ramp on the same bill; VRPlatform accepts the same bill
moving from Failed back to Requested or Processing and retains the earlier
attempt. To pay a payout again from VRPlatform after Failed or Canceled,
archive it and create a replacement with recreatedFromTransactionId. After a
Returned payout, record compensation and create a replacement against the
still-active original payout.
An expense request that fails before a Ramp bill is created returns to Unpaid, and Pay becomes available again. A terminal Ramp payment failure also returns the expense to Unpaid. After Failed, Returned, or Canceled, Pay reschedules the same Ramp bill unless it has been permanently archived.
Once any Ramp ACH attempt is recorded, ordinary financial edits remain locked.
The offline Pay endpoint permits a payment-only update after permanent bill
archive as described above. It does not unlock other expense fields.
An expense may change from Paid to Unpaid when Ramp reports Failed, Returned,
or Canceled, and that terminal state permits its next same-bill Pay request.
For this Unpaid reset, PUT /transactions/{id} accepts
payment: {status: "unpaid"} with optional accountId: null and
payment.date: null; it removes the payment posting subject to accounting locks.
Status-only archive or unarchive is allowed only after durable Ramp bill
archive or verified no-bill recovery evidence. DELETE /transactions/{id}/provider-payments/{providerPaymentId} is deprecated and
always rejects; payment history cannot be unlinked.
Use
POST /transactions/{id}/provider-payments/{providerPaymentId}/cancel for a
Requested or Processing payment. The body must state archivePayout. Ramp,
not initiatedAt, decides whether the payment and bill can still be archived.
When archivePayout=false, the provider operation can proceed even if local
accounting is locked. Expense cancellation uses expensesToBills, cancels only
the payment, keeps the Ramp bill active, and returns the expense to Unpaid after
Ramp confirms cancellation. Payout cancellation retains its existing behavior.
The field retains its payout-oriented name for client compatibility.
Use
POST /transactions/{id}/provider-payments/{providerPaymentId}/archive for a
Failed, Returned, or Canceled payout or expense. Ramp bill archive completes
before the requested local accounting archive. Returned payouts retain paid
accounting and use a separate compensation for the returned cash. Failed and
canceled transactions, and returned expenses, archive local accounting. When a
failed attempt has only a durable request sync and no Ramp bill source or
reference, the API records verified no-bill evidence and archives locally
without calling Ramp. A provider-success/local-lock failure keeps the provider
result and can resume the accounting archive later. Paid provider payments
cannot use recovery archive.
Cancel and Archive return the standard queued operation acknowledgement. Poll
GET /operations/{operationId} until the
provider-payment-recovery operation becomes completed, failed, or
canceled, then refresh the transaction and provider-payment history as the
canonical outcome. A local no-bill archive can already be completed on the
first poll. This status read requires operations:read, not
integrations:read; sync details remain internal.
A replacement expense or failed or canceled payout must use an inactive source
of the same type. A returned payout remains active and can be replaced only up
to its active compensated amount. Existing replacement links remain unique.
Unarchive does not call Ramp and cannot create another active chain member.
Returned is assigned only from Ramp Transfer API RETURNED_BY_RDFI, not from
undocumented Bill status aliases.
When a replacement payout omits uniqueRef, the API assigns a new
PAY-YYYYMM-XXXXX reference from the replacement date. It never reuses the
source payout reference. Ramp dispatch rejects a payout without a stored
reference before provider work is queued, and transaction reads expose the
Pay action as unavailable.
POST /transactions/{id}/pay supports dryRun=true; dry runs persist no sync
or accounting change and dispatch no provider work. Provider recovery
endpoints do not support dry run because a database-only preview cannot
simulate Ramp.
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 and dates that are not due 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.
Running a template queues its due occurrences. 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
