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:
GET /connections/pms-cutover/listing-mappings— see how new PMS listings map onto existing listings.POST /connections/pms-cutover/preview— dry-run the cutover and review impact counts and blockers.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
accountingStartAtset (often1900-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
accountingEndAtand the targetaccountingStartAtto 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:
| Status | Meaning | Apply behavior |
|---|---|---|
alreadyShared | Ref already points at a source listing | Nothing to do |
matched | Exactly one source listing matches | Merged into the source listing |
ambiguous | Multiple source listings share the name | Blocks apply until resolved |
unmapped | No source listing matches | Kept 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, andlockedCountblockers.listingContinuity— match counts per status, how many target reservations, transaction lines, and payment lines would move, andlockedCountfor listing moves that would touch locked journal history.
Resolving blockers
| Blocker | Cause | Resolution |
|---|---|---|
impactedReservations.lockedCount | Reservations in the window with active journal entries attached to a non-draft owner statement, or dated before booksClosedAt | Unpublish the statement or repair the stale postings before cutover |
listingContinuity.ambiguousCount | A target ref name matches multiple source listings | Send an explicit listingMappings entry |
listingContinuity.lockedCount | A listing merge would rewrite journal entries that are statement-attached or books-closed | 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
sourceListingIdmerges the target ref into that source listing. nullkeeps 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/applyApply re-runs the full preview validation immediately before writing, then in one transaction:
- sets the source
accountingEndAtand targetaccountingStartAttocutoverAt; - 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) andREFRESH_TRANSACTION_JOURNALeffects 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.
