VRPlatformVRPlatform
Build a Product UI

Reservations

Build booking, financial-line, adjustment, and mapping workflows

Last Updated: 2026-09-10 Version: 1.1

Mental Model

A reservation connects booking identity and stay context to accounts receivable, revenue, fees, deposits, adjustments, and owner statements. The API constructs accounting output; the client renders it.

Resources and Lifecycle

Reservations contain listing, guest, stay dates, source identity, booking status, payment lines, adjustments, and calculated financial output.

adults and children preserve the PMS occupancy breakdown. guests remains the combined count used by existing integrations and fee formulas. Reservation detail, list, and CSV expose all three values. Owner-statement layouts can use reservation.adults or reservation.children as numeric Net Revenue fields; existing reservation.guests columns retain the combined count.

The reservation's listing is required accounting context. Background connection, mapping, recurring-fee, recognition, and tax changes refresh only reservations that retain that canonical listing link. They do not infer a listing from a provider connection. An explicit refresh of legacy data without the link fails so the reservation can be repaired instead of posted to an assumed listing.

Writes accept the booking statuses booked, cancelled, canceled, inquiry, and inactive; both cancellation spellings are equivalent on write. Reads always return booked, canceled, or inactive. Booking status and generalLedgerStatus=active|inactive are separate. The normal reservation update (PUT /reservations/{id}) changes booking status; the dedicated status update (PUT /reservations/{id}/status) activates or inactivates reservation journal entries.

When a PMS batch changes a cancelled or canceled reservation back to booked, the API clears the previous cancelledAt value even when the PMS omits that field. Other status transitions retain their existing timestamp behavior. Reservation and journal locks still apply to the complete batch update.

Creation enforces both reservation identities used by storage. A connection cannot reuse a uniqueRef, and a non-inactive reservation cannot reuse the same connection, resolved listing, and confirmationCode, even when its uniqueRef differs. Either collision returns a user-correctable HTTP 400 and persists nothing. An inactive reservation has no calculated connection, listing, and confirmation identity, matching the storage constraint.

Any transaction line or journal entry that explicitly links a reservation must reference a reservation in the same team. A missing or different-team ID is rejected before accounting data is written and is never followed across team boundaries.

Read Model

Render returned financial lines, resolved accounts, posting dates, totals, paid state, locks, and issues. Do not recreate cancellation, deferred revenue, fee calculation, or journal generation in the client.

financials.totals.openingGuestReceivable identifies the part of the guest receivable carried into accounting on the day before the team's accounting start date. It remains part of financials.totals.guest and payment.expected, so payment status continues to represent the complete amount due. Render the opening amount separately from charges recognized in the active accounting period.

A recurring-fee refresh updates the journal and the reservation's materialized manager, owner, tax, and payment totals together. Reservation list and detail therefore reflect the same current fee postings, including deductions from negative formula-account balances such as channel commissions.

Reservation list and detail share one materialized issues snapshot. Detail reads compute issues from live data and write them back to the snapshot. List reads serve that stored snapshot: issuesComputedAt is the completion time of its latest durable evaluation (null before the first one), and issuesStale=true means a committed change has a newer revision. The returned issues are the previous snapshot, or an empty pending snapshot for a new reservation. Keep rendering the snapshot, show pending freshness where relevant, and re-read while it is stale. Do not merge it with client-side issue calculations.

Committed reservation and account mutations mark affected snapshots and wake a serialized regional evaluator. Required effects repeat invalidation after they commit. A minute recovery task seeds and reconciles bounded batches and keeps draining while durable work remains. An immediate list re-read can briefly still return the previous snapshot. Issue computation stays out of list reads, so they do not expand journal entries and accounts for each reservation.

Financial lines omit the generated A/R mirror leg of a reservation adjustment, including each daily leg of a pro-rata adjustment. The adjustment itself and genuine deposit or other A/R-account activity remain visible. Do not recreate the accounting mirror in the client. The mirror remains an active journal entry because it balances an adjustment posted independently from the reservation A/R date. This omission applies only to the reservation financial folio; general-ledger and report surfaces must apply their own documented accounting rules.

For a booked, accounting-active reservation whose posting date is before the accounting start, a refresh persists its base journal entries as inactive with inactiveReason=generalLedgerStartAt. Until that refresh, detail reads can still derive the mapped, non-excluded guest charge without journal entries. In either shape, reservation lines have status=inactive. A financial line inactive only because it falls before the accounting start returns inactiveReason=priorToStartDate, matching the priorToStartDate issue issues carries with context.startDate after issue evaluation. Render that warning and the inactive line state instead of treating booking status=booked as proof that the reservation posts to accounting.

A reservation-level generalLedgerPostingAt supersedes its default recognition date when evaluating the PMS accounting window and generating base journals. Refreshing with an open date at or after the accounting start moves the base entries to that date, activates them, and recalculates journal-backed payment and financial totals. A manually generalLedgerStatus=inactive reservation remains a separate explicit exclusion and keeps its zero expected-payment behavior.

Use returned source and connection state to decide which synced fields the user can edit.

For reservation preflight, pass comma-separated reservation IDs through the ids filter. An inclusive ID request is a targeted lookup: every accessible matching reservation is returned even when normal browsing hides it because of booking, connection, source, or inactive-ownership accounting state. The pagination total reflects the targeted matches. Prefixing the filter with ! keeps normal browse rules and excludes the listed reservation IDs.

Date Filters

GET /reservations and GET /reservations/csv accept the same date and dateField filters. Use dateField=cancelledAt to match the reservation's cancellation timestamp. A single date matches that calendar date; a start...end value includes both boundary dates. Other supplied filters, such as status=canceled, still apply.

CSV Export

GET /reservations/csv derives its financial view from the authenticated token. For owner users, Total is the reservation's owner revenue, matching the owner portal reservation table. A missing owner total produces 0.00. For manager users, Total, Paid, and Unpaid retain the guest payment amounts. Owner exports omit Paid and Unpaid, and team settings can hide Total or canceled reservations. Every export includes separate Adults, Children, and combined Guests columns.

Payment Status

Use the returned payment.expected, payment.received, and payment.status on reservation detail, list, and owner-statement rows. These surfaces apply the same journal-backed payment rule.

A co-host payout leaves only the manager share collectible while its active accounts-receivable payments remain below the full guest charge. When its A/R leg and other active A/R payments meet or exceed the guest charge, expected remains the full charge and received remains the actual A/R amount. An exact payment is paid; an excess remains overpaid. This covers full and remaining-balance reclasses plus later channel adjustments. Do not infer either case from the presence of a co-host line alone. Archiving or restoring a deposit recalculates linked reservation payment status after its journal entries change status.

Imported payment and transaction lines with a reservation confirmation or customer reference are linked in the background. Newer lines that cannot be matched do not prevent older candidates from being considered.

The reservation-list paidStatus filter accepts unpaid, underpaid, overpaid, and paid. Prefix one of those values with ! to exclude it, such as paidStatus=!paid. Any other value is rejected as an invalid query; clients must omit an unset filter instead of sending strings such as undefined or null.

Decision Table

Context changeRecalculate or validate
Booking status (booked, cancelled/canceled, inquiry, inactive)Active financials, adjustments, and journal consequences
generalLedgerStatusJournal activity without changing booking status
Listing or stay datesOwnership, recognition, fees, and posting dates
Channel or line typeLine mapping and recognition override
Adjustment partyOwner-period eligibility and locks
Recurring fee adjustmentFee posting accounts and metadata
generalLedgerPostingAtReservation PMS boundary and journal date; adjustment journal date

Adjustments and Posting Dates

An adjustment can specify a line type or account, amount, description, owners|manager party, optional recurring fee, and optional generalLedgerPostingAt.

Create adjustments through the reservation update or dedicated adjustment endpoint. Update and delete adjustments through PUT and DELETE /reservations/{reservationId}/adjustments/{id}.

Recurring-fee adjustments use their own balanced journal group. If the active fee references an archived posting account, create or update returns a user error before persistence; it does not inactivate the reservation's revenue, deferred-revenue, or A/R rows. Repoint or deactivate the fee before retrying. The final materialized journal also validates derived deferred-revenue postings. An inactive derived posting account taints the refresh and preserves the existing journal instead of writing a partial replacement.

An explicit mutation of an independently balanced adjustment can post or remove its postings while its parent reservation is archived with status=inactive or has generalLedgerStatus=inactive. The mutation refreshes only that targeted adjustment group; it does not reactivate the reservation or the rest of its journal. Books-closed, statement-period, and statement-attachment locks still apply.

When a recurring-fee adjustment changes a dependent fee, any historical fee group inside a published statement period stays unchanged, including a group that has no journal rows yet. The adjustment still posts its own balanced entries on its open posting date. An unlocked dependent fee is recalculated.

When creating an adjustment for an active booking, provide either type or recurringFeeId; selecting only accountId does not classify the line. The recurring fee must exist in the current team. A missing, deleted, or other-team fee returns NOT_FOUND before the adjustment is saved. Canceled bookings may preserve an unclassified adjustment because their financial treatment is derived from the cancellation state.

Owner adjustments participate in ownership and statement locks, while manager adjustments normally avoid owner-period locking. A separate open posting date is required when a new owner adjustment would otherwise reuse a locked reservation A/R date. Use returned date previews rather than assuming stay, cancellation, or A/R date.

For a reservation that spans the accounting start, an owner adjustment posted before that date remains inactive revenue but participates in the opening guest balance. It reduces or increases opening A/R and receives a matching opening-balance assignment. An adjustment posted on or after the start keeps its independent A/R mirror. This lets pre-start reversals remove imported charges without stacking replacement post-start revenue onto the guest total. An owner adjustment dated before the accounting start is what reduces an opening guest receivable, so it is the supported correction when a reservation's guest total does not net to zero. Updating or deleting an adjustment that already contributes to that shared opening-balance group rebuilds the reservation journal together. The mutation is rejected if another row in the affected group is locked.

For adjustment creation, provide a concrete generalLedgerPostingAt when the posting date is a user decision. Without one, the route keeps the natural date when it is open and otherwise resolves and persists the earliest date allowed by books closing and statement-period locks. Pass onLocked=error to reject a locked natural date instead. onLocked=nextOpen remains available explicitly, does not bypass journal locking, and cannot be combined with an explicit posting date.

The dry-run response includes the resolved concrete adjustments[].generalLedgerPostingAt without saving the adjustment. The policy applies only when creating an adjustment; adjustment updates continue to require an explicit posting date when moving the line.

Line Mappings and Revenue

Payment-line mappings translate PMS or channel line types into accounts and can override recognition. Recognition can use check-in, check-out, booked-at, or pro-rata dates. Pro-rata can cross months and ownership periods.

When an owner adjustment is recognized on a different date from the shared reservation receivable, deferred-revenue postings bridge those dates. An adjustment with its own matching receivable uses that pair instead.

After a PMS is connected, centrally managed defaults for that PMS bootstrap only missing team mapping rows. Existing mapped and explicitly excluded rows are never replaced. Mapping pages jump between the selected team's distinct indexed reservation line types and union explicit mappings. Work scales with the number of line types rather than the number of historical reservation lines, and an asynchronously reported catalog cannot hide an existing type.

For a pro-rata owner adjustment, each recognized daily amount and its matching accounts-receivable amount post on the same date. This includes uneven cent splits; callers do not need to force a separate posting date to keep the generated journal balanced.

When a reservation crosses the accounting start date but its reservation-level A/R remains excluded before that date, a line recognized after the start is bridged from opening equity. For a pro-rata line, only the post-start portion uses that bridge; earlier portions remain inactive, and Deferred Revenues clears when the post-start revenue is recognized.

For a reservation sourced from a child in a listing group, journal entries keep the child listing as their source and resolve ownership through the group parent. This includes deferred-revenue legs and rebalances. A missing-ownership issue is returned only when neither the stored period nor an effective parent period covers the posting date.

To deliberately exclude a line type from journal posting, create or update its mapping with accountId: null. Use a channel mapping with accountId: null to exclude only that booking channel. Mapping status=excluded and reservation line isExcluded=true are derived read-only results; omitting a line from a reservation payload is not the exclusion control.

A single-reservation journal refresh with an active unmapped line writes nothing and returns Map or exclude unmapped reservation lines in Reservation Accounts before refreshing journal entries. Its structured error context contains unmappedReservationLines[] with each line type and description so the caller can link directly to the required mapping action.

For POST /reservations/journal-entries, exactly one explicit ID completes before success and returns total: 1. Multiple IDs or an omitted selection queue a reservation-journal-refresh operation. Follow the returned operation until it is terminal; empty or repeated ID collections are invalid. If accounting locks prevent the queued refresh, the operation fails with failure.code=journalLocked. This also applies when the operation uses a UUID identifier. The journal remains protected; retrying does not reopen published history.

Bulk background refreshes run in bounded batches per team. Larger selections can remain queued while earlier batches finish; poll the operation for progress.

Reservation list and detail reads return journalStatus. Render recalculating while queued or running refresh work can still change the journal, failed for an unresolved terminal refresh failure, and stale when known journal changes have no active recovery. Only current proves that every known reservation journal change is applied. Use the embedded operations to poll work, then re-read the reservation after it becomes terminal.

For a failed refresh without a more specific configuration or balance error, journalStatus returns reason: journalLocked and action: reviewAccountingLock when current reservation locks prevent a strict full refresh. This also applies to older failures recorded without a typed lock cause. Review whether a correction belongs in an open period; retrying does not remove the lock. The failure and unapplied changes remain recorded. Current or actively recalculating journals do not become failed merely because the reservation has locked entries. For statement and recurring-fee statuses, this lock guidance applies only when every outstanding reservation is currently locked. A specific configuration or balance failure keeps its own recovery action.

Every non-current status includes a typed reason and supported action. Wait for an active refresh, retry pending or generic failed work, fix reported journal configuration, contact support for an unbalanced result, or review an accounting lock and use an open-period adjustment when a correction is needed.

Editability

Discover outstanding refresh failures through Portfolio Health, then request the affected reservation's preview for current blocking details.

Use GET /reservations/{id}/journal-preview to review a strict full refresh without changing journals or freshness tracking. It requires reservation-read access and is unavailable to owners. The response reports a finite reason, recovery action, and proposed insert/update/delete counts. changes: null means calculation stopped or was unavailable, not that no changes are needed. A blocked preview is a successful HTTP 200 response. Request failures use the standard HTTP error envelope.

lock uses the same shape as reservation and transaction locks: booksClosed.date, ownerStatement.ids, or statementPeriod.hits with listingId, txnAt and openFrom. It describes this refresh, not every possible edit to the reservation. lock: null means lock validation was not completed; do not interpret it as unlocked.

issues uses the shared code, severity, message, and typed context convention:

  • journalLocked is a review warning. context.journalEntriesCount counts blocked posting checks, not distinct entries, and is null when no detailed check is available. context.entries contains up to five examples with available entry, account, listing and statement IDs and posting dates. The lock and examples cover only the first failing layer. Statement-period example statement IDs are links to the period blocker; they do not imply an entry attachment. Attachment IDs appear in lock.ownerStatement.ids.
  • journalConfigurationInvalid is an error. context.reasons identifies missing accounts, inactive accounts, fee formula errors, missing fee data or missing inactive reasons. Available line, account and recurring-fee IDs identify what needs attention. Internal posting keys, provider payloads and formula error text are excluded.
  • journalUnbalanced is an error. context.currencyLedgerImbalances reports signed differences in minor currency units per currency/ledger. context.postingGroupImbalances adds the posting date. These are two views of imbalance; do not sum them together or present them as an owner-statement or payout change.

For example, closed books can return:

{
  "status": "blocked",
  "reason": "booksClosed",
  "action": "reviewAccountingLock",
  "changes": null,
  "lock": { "status": "locked", "booksClosed": { "date": "2026-08-01" } },
  "issues": [{
    "code": "journalLocked",
    "severity": "warning",
    "message": "Books closing prevents this journal refresh",
    "context": {
      "journalEntriesCount": 1,
      "entries": [{ "txnAt": "2026-07-01" }]
    }
  }]
}

unchanged and ready responses have an empty issues array. Inactive accounting returns unchanged / journalInactive, with null changes and lock. An unavailable preview also has null changes and lock and no domain issues; show its unavailable state. The schema pairs each reason with its supported status and action.

A ready preview permits offering the existing refresh action to users with journal posting permission and an unlocked reservation, with no refresh already running. It is not a guarantee of future success: refresh rechecks current locks and source data before writing. A preview never changes journalStatus or enables finalization/payment enforcement.

Reservation changes can be blocked by books closing, statement periods, statement attachments, inactive ownership, or source ownership of synced fields. Use the most specific entity or adjustment lock.

An adjustment with an independent open posting date can remain editable while the reservation's owner-side A/R posting is locked. Updating or deleting an adjustment is blocked when its entries are statement-attached, in closed books, or in a statement-locked period. This includes inactive pre-start entries: their status excludes them from totals but does not make their posting history editable. Use adjustments[].lock for the adjustment resource and the matching financials.lines[].lock for a rendered financial row to disable the mutation before submission. Each lock covers every journal leg with the same line ID.

A full or targeted fee, adjustment, or A/R journal refresh can return a structured journalEntries lock when the persisted journal and complete desired journal are balanced, but applying only the unlocked changes would split a balanced group. The refresh writes nothing in that case. Treat it as locked work; a genuinely unbalanced desired journal remains an internal accounting error.

For a single-reservation refresh, this denial returns HTTP 403 with the lock reason and affected-entry count. bypassStatementPeriod does not bypass entries attached to owner statements or closed books.

Preview and Preflight

Create, update, batch, booking/accounting status, removal, and adjustment mutations expose dry run where the generated OpenAPI declares the dryRun parameter. Journal regeneration and line-mapping changes intentionally skip generic dry run because they require consequence-oriented results.

Recalculate financial previews when channel, line type, mapping, stay dates, recognition settings, party, fee, or posting date changes.

Mutation Recipe

  1. Load the reservation, financial output, mappings, and returned locks.
  2. Apply source-editability and deterministic status rules.
  3. Recalculate affected account and posting-date previews.
  4. Add an independent adjustment posting date when required.
  5. Dry-run the complete reservation or adjustment payload.
  6. Render structured lock and mapping issues.
  7. Confirm and apply.
  8. Re-read the reservation and render returned accounting output; continue polling issue state while issuesStale=true.

Failure and Recovery

A missing mapping means the user needs to configure the exact source line type. Locked owner history calls for showing the relevant period, statement, or first open date rather than silently changing the adjustment to manager. After a source sync or concurrent mutation, reload before retrying. An inactive fee posting account requires fee or account remediation; retrying the adjustment without changing that configuration cannot succeed.

Common Recipes

Cancel a paid reservation

Update booking status through the reservation mutation, then render returned issues and adjustments. Do not locally synthesize refund journals.

PUT /reservations/{reservationId}?dryRun=true
Content-Type: application/json

{
  "status": "canceled",
  "cancelledAt": "2026-07-19"
}

Post a late owner adjustment

Choose owners, create with onLocked=nextOpen&dryRun=true, inspect the returned posting date and financial lines, then repeat without dryRun to apply. Use an explicit generalLedgerPostingAt instead when the posting date is a user decision.

POST /reservations/{reservationId}/adjustments?onLocked=nextOpen&dryRun=true
Content-Type: application/json

{
  "type": "lateFee",
  "amount": 2500,
  "description": "Late owner charge",
  "party": "owners"
}

Release or forfeit a security deposit

Map the PMS reservation's refundable deposit line to Security Deposits Payable. When captured principal is refunded, create an adjustment that reverses that liability line. When it is forfeited, create equal-value adjustments that reverse the liability and reclassify it to the explicitly selected revenue account and owners or manager party.

Use an explicit open generalLedgerPostingAt, or dry-run and apply with onLocked=nextOpen. The adjustment posts its own balanced journal group, so it can correct a locked or GL-inactive reservation without reopening the booking or changing the closed period. Record the related cash refund or processor release as a Transaction against the asset that moved. See Security Deposit Accounting for channel-held settlement and chargeback boundaries.

Webhook Reconciliation

reservation.changed is emitted after a committed public aggregate change and after required journal or payment effects finish. No-ops, dry runs, rollbacks, and journal-only work that leaves the public response unchanged do not emit. The compact body contains no guest, stay, amount, payment, source, or line data; compare resourceVersion and fetch href.

API Reference

On this page