VRPlatformVRPlatform
Integrate & Migrate Data

PMS Migration

Plan a clean accounting cutover between property management systems

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 — revenue posts on the check-in date (the default);
  • checkOut — revenue posts on the check-out date;
  • proRata — revenue is spread across the nights of the stay;
  • bookedAt — revenue posts on the booking date; retired for new configuration, but stored values keep working.

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

  • impactedReservations.lockedCount means reservations in the window have journal entries attached to a non-draft owner statement or are dated before booksClosedAt. Unpublish the statement or repair stale postings before cutover.
  • listingContinuity.ambiguousCount means a target ref name matches multiple source listings. Send an explicit listingMappings entry.
  • listingContinuity.lockedCount means a listing merge would rewrite statement-attached or books-closed journal entries. Resolve 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 matched source 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 --get 'https://api.vrplatform.app/connections/pms-cutover/listing-mappings' \
  --data-urlencode 'sourceConnectionId=SRC' \
  --data-urlencode '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.

API Reference

  • GET /connections/pms-cutover/listing-mappingsContract
  • POST /connections/pms-cutover/previewContract
  • POST /connections/pms-cutover/applyContract

On this page