VRPlatformVRPlatform

PMS Migration

Move a team from one property management system to another with a clean accounting cutover

PMS Migration

When a team replaces its property management system, accounting must switch from the old PMS to the new one on a single cutover date — without double counting reservations, breaking published owner statements, or splitting listing history across duplicate listings.

The PMS cutover endpoints handle this as one reviewed operation:

  1. GET /connections/pms-cutover/listing-mappings — see how new PMS listings map onto existing listings.
  2. POST /connections/pms-cutover/preview — dry-run the cutover and review impact counts and blockers.
  3. POST /connections/pms-cutover/apply — execute the cutover in one transaction.

Concepts

Accounting windows

Every PMS connection can carry an accounting window (accountingStartAt / accountingEndAt). Only reservations whose accounting date falls inside the window post to the general ledger.

  • The current PMS has accountingStartAt set (often 1900-01-01) and no end.
  • The replacement PMS is connected in parallel without window dates. It syncs reservations and listings, but stays out of live accounting.
  • The cutover sets the source accountingEndAt and the target accountingStartAt to the same cutover date in one transaction.

The accounting date per reservation follows the team's revenue recognition setting: checkIn (default), checkOut / proRata, or bookedAt.

Listing continuity

The replacement PMS imports its own listing refs. Without intervention, each ref would create a duplicate listing — splitting ownership periods, statements, and journal history across two buckets for the same property.

The cutover matches target PMS listing refs to existing source listings by exact normalized name, using the address only to break ties between identical names. Every target ref gets one of these statuses:

StatusMeaningApply behavior
alreadySharedRef already points at a source listingNothing to do
matchedExactly one source listing matchesMerged into the source listing
ambiguousMultiple source listings share the nameBlocks apply until resolved
unmappedNo source listing matchesKept separate as a new listing

unmapped refs are normal — teams onboard new properties on the replacement PMS. They do not block the migration.

1. Review listing mappings

GET /connections/pms-cutover/listing-mappings
  ?sourceConnectionId=...&targetConnectionId=...

Returns one row per target PMS listing ref plus the source listing pool for building a mapping UI:

{
  "sourceListings": [
    { "id": "listing-a", "name": "Twin Cabin", "address": "12 Forest Rd" },
    { "id": "listing-b", "name": "Twin Cabin", "address": "14 Forest Rd" }
  ],
  "targetListings": [
    {
      "targetListingConnectionId": "ref-1",
      "uniqueRef": "hostaway-123",
      "name": "Twin Cabin",
      "address": null,
      "listingId": "target-only-listing",
      "status": "ambiguous",
      "suggestedListing": null
    }
  ]
}

suggestedListing is the source listing the automatic matcher would merge into — it is exactly what apply will do when no explicit mapping is sent.

2. Preview the cutover

POST /connections/pms-cutover/preview
{
  "sourceConnectionId": "source-pms-uuid",
  "targetConnectionId": "target-pms-uuid",
  "cutoverAt": "2026-05-01"
}

Preview is read-only and reports:

  • impactedReservations — source/target reservations in the cutover window, how many journals will refresh, and lockedCount blockers.
  • listingContinuity — match counts per status, how many target reservations, transaction lines, and payment lines would move, and lockedCount for listing moves that would touch locked journal history.

Resolving blockers

BlockerCauseResolution
impactedReservations.lockedCountReservations in the window with active journal entries attached to a non-draft owner statement, or dated before booksClosedAtUnpublish the statement or repair the stale postings before cutover
listingContinuity.ambiguousCountA target ref name matches multiple source listingsSend an explicit listingMappings entry
listingContinuity.lockedCountA listing merge would rewrite journal entries that are statement-attached or books-closedResolve the lock or map the ref to null to keep it separate

To resolve ambiguity (or override a suggestion), pass listingMappings on preview and apply:

{
  "sourceConnectionId": "source-pms-uuid",
  "targetConnectionId": "target-pms-uuid",
  "cutoverAt": "2026-05-01",
  "listingMappings": [
    { "targetListingConnectionId": "ref-1", "sourceListingId": "listing-a" },
    { "targetListingConnectionId": "ref-2", "sourceListingId": null }
  ]
}
  • A sourceListingId merges the target ref into that source listing.
  • null keeps the ref separate as a new listing.
  • Unknown target refs, ids that are not source listings, and duplicate target refs are rejected.

3. Apply the cutover

POST /connections/pms-cutover/apply

Apply re-runs the full preview validation immediately before writing, then in one transaction:

  • sets the source accountingEndAt and target accountingStartAt to cutoverAt;
  • sets source reservations on or after the cutover date to inactive;
  • moves matched target listing refs, target reservations, and listing-linked transaction/payment lines onto the source canonical listings;
  • re-links deposit and payment lines from retired source reservations to the matching target reservations (by reservation matcher fields, then by source reservation identifiers); lines without a target match are detached;
  • queues REFRESH_RESERVATION_JOURNAL (strict lock policy) and REFRESH_TRANSACTION_JOURNAL effects for everything that changed.

The response is the preview payload plus applied: true and queuedReservationRefreshCount.

Example

# 1. What needs mapping?
curl 'https://api.vrplatform.app/connections/pms-cutover/listing-mappings?sourceConnectionId=SRC&targetConnectionId=TGT' \
  -H 'x-api-key: your-api-key' \
  -H 'x-team-id: your-team-id'

# 2. Preview
curl 'https://api.vrplatform.app/connections/pms-cutover/preview' \
  -X POST \
  -H 'content-type: application/json' \
  -H 'x-api-key: your-api-key' \
  -H 'x-team-id: your-team-id' \
  --data '{
    "sourceConnectionId": "SRC",
    "targetConnectionId": "TGT",
    "cutoverAt": "2026-05-01"
  }'

# 3. Apply with explicit mappings
curl 'https://api.vrplatform.app/connections/pms-cutover/apply' \
  -X POST \
  -H 'content-type: application/json' \
  -H 'x-api-key: your-api-key' \
  -H 'x-team-id: your-team-id' \
  --data '{
    "sourceConnectionId": "SRC",
    "targetConnectionId": "TGT",
    "cutoverAt": "2026-05-01",
    "listingMappings": [
      { "targetListingConnectionId": "ref-1", "sourceListingId": "listing-a" }
    ]
  }'

Rules

  • Source and target must be different active PMS connections in the same team.
  • The source must have accountingStartAt; the target must have no window.
  • The cutover date must be after the source accounting start and not before the team's booksClosedAt.
  • A third PMS connection whose window overlaps the post-cutover range is rejected.
  • Apply rejects locked impacted reservations, unresolved ambiguous listing refs, and listing moves that would touch locked journal history.

On this page