VRPlatformVRPlatform
Integrate & Migrate Data

Legacy (VRI) Migration

Move a non-GL team into GL and import historical owner statements

VRI is the legacy VRPlatform product generation that ran without a general ledger; VRT is the current generation with full general-ledger accounting. Migrating a VRI team to a VRT team is a staged flow: bootstrap the new team, map legacy statement accounts and listings onto the target team, then import the historical owner statements.

  1. POST /partner/vri-to-vrt — create the GL team from the legacy source and provision its chart of accounts, statement layouts, and recurring fees.
  2. GET /partner/vri-to-vrt/statements/account-mappings — see which legacy accounts need mapping.
  3. GET /partner/vri-to-vrt/statements/listing-mappings — see which legacy listings need mapping.
  4. POST /partner/vri-to-vrt/statements — queue a historical-statement import with explicit mappings, then poll its operation status.

For a team migrated before booking-channel classification migration was added, run POST /partner/vri-to-vrt/booking-channels without rerunning bootstrap or importing statements.

The VRPlatform app has no VRI migration screen. Run the migration through these endpoints directly or through the VRPlatform MCP. VRPlatform admins can still link an existing GL team to its VRI source from the team menu.

Use a partner identity that manages the source and target teams. A Partner API key needs the partner:provisioning:v1 bundle for bootstrap and partner:general-ledger:v1 for mapping previews and statement import. An OAuth user needs the equivalent access on the selected partner team. A partner cannot migrate or inspect teams managed by another partner.

With MCP OAuth, pass the partner team as teamId or set team_id on the MCP connection. The MCP forwards that value as x-team-id; source and target team ids remain in the documented query or body fields. Partner API-key MCP calls derive the partner from the key and omit teamId.

When another workflow creates the GL target with POST /teams, include the source relationship in that same request:

POST /teams
Content-Type: application/json

{
  "name": "Mountain Papa",
  "email": "operations@example.com",
  "generalLedger": true,
  "migratedFromTenantId": "legacy-team-uuid",
  "uniqueRef": "mountain-papa-vrt"
}

The target must be GL-enabled, and the source must be a non-GL team managed by the same partner. The source is resolved through the control plane, so it may live in any data region; legacy non-GL teams live in crunchy, GL targets may be in any region. Partner API keys must also supply the idempotency identity uniqueRef. Invalid relationships are rejected before either team record is created. This generic create contract records the relationship; use the dedicated bootstrap below when the target also needs legacy tenant data copied and GL resources initialized as one migration workflow.

If the GL target already exists, select it with x-team-id and link it before requesting mapping previews:

PUT /team
Content-Type: application/json

{
  "migratedFromTenantId": "legacy-team-uuid"
}

A restricted key may make this link with partner:provisioning:v1. The body must contain only a non-null migratedFromTenantId; renaming the team or combining the link with another team edit requires teams:write, which the current early-access provisioning bundle grants. Every non-null migration source in a sole or mixed update must still belong to the calling partner and be non-GL, the target must be GL-enabled, and an existing link to another source is rejected. An existing link cannot be cleared or changed through the API. The link marks both teams as one migration for billing, reporting, and deletion exclusion; setting it is recorded as migratedFromTenantId on the team audit event. The historical-statement previews, the historical-statement import and the reservation setup read below resolve the source and target regions independently. Only the bootstrap requires both teams in one region. A mapping preview or import for a target that is not linked fails with the reason migrationLinkMissing.

Concepts

Effective cutover

glStartAt on the bootstrap request is the minimum requested cutover. The migration pushes the effective cutover forward to the first day of the month after the source team's last posted/published owner statement, so live GL accounting never overlaps months the legacy team already settled.

PMS accounting source

Bootstrap rebuilds PMS accounting windows for the GL target instead of copying the source dates. The source's single current PMS receives accountingStartAt = effectiveGlStartAt with no end date. Other copied PMS shells have no accounting window, so they remain outside live GL accounting.

When multiple PMS connections are active, an existing single open accounting window identifies the current source. If no single current source can be identified, bootstrap rejects the migration before writing the target team.

Booking channels

Bootstrap copies the booking-channel classifications used by the source team's reservations into the shared control plane, then queues their projections to every data region. This preserves labels such as Airbnb, Booking.com, VRBO, and Direct when the target PMS sends only provider aliases.

Historical-statement import repeats this step so an existing migrated team can be repaired safely. An existing global classification must match the source; a conflict rejects the migration instead of overwriting shared configuration. Unclassified aliases remain unavailable to booking-channel selectors.

Historical ledger

Imported statements and their journal entries land on ledger = historical. They preserve the owner-facing history (balances, statement PDFs/views) without mixing into the live general ledger that starts at the cutover.

Account mappings

Legacy statements reference VRI accounts that do not exist on the new chart. Each legacy account found on importable statement lines must resolve to one target account. A hardcoded mapper covers the common cases; everything else needs an explicit mapping — the same fetch-then-post pattern as PMS migration listing mappings.

POST /partner/vri-to-vrt/statements checks the mappings before it queues the import. A missing account mapping or an unmatched legacy account returns 400 with the legacy account keys in context.legacyAccountKeys, and a missing listing mapping returns 400 with the source listing ids in context.sourceListingIds. No operation is created. Page the mapping previews until hasMore is false: an account that appears only on a later page is still required.

Unmatched accounts

A statement line keeps the account name it was posted under. When a legacy account was renamed afterwards, the old name matches no account. The import no longer guesses a parent or last-segment match, because a guess can land on an account that no layout shows and skip the line without notice. Each unmatched name needs an alias to the account it stands for, or an exclusion. An aliased line is read as that account's statement line, so it appears in the section the legacy layout gave that account. It still needs a target account like any other account key.

Listing mappings

Historical statements must attach to target VRT listings and ownership periods. Teams created by the bootstrap flow already have deterministic target listing ids. Manually created VRT teams often have different real listing ids, so the statement import supports explicit legacy-to-target listing mappings.

When an explicit mapping targets an existing VRT listing, the import reuses the native ownership period active at the effective cutover and extends its start to inception. Historical statements attach to that period instead of creating a duplicate period ending at the cutover. A rerun removes the replaced imported period after its statements and journal entries have moved. If no eligible native period exists for a mapped listing, its imported period remains the historical destination. Later real owner changes remain separate periods. When the listing already has an earlier period with different owners, the native period keeps its start date and the earlier period keeps its own statements, so periods never overlap.

If an imported historical reservation has since acquired a live journal reference, a rerun preserves that reservation id and updates its listing in place. Corrected listing mappings therefore do not detach payouts, deposits, or other live accounting rows from the reservation they already reference.

Use targetListingId to attach legacy statement history to an existing VRT listing. Use targetListingId: null only when the legacy listing has no VRT match and should be imported as an inactive historical-only listing.

1. Bootstrap the GL team

POST /partner/vri-to-vrt
{
  "sourceTeamId": "legacy-team-uuid",
  "targetName": "Mountain Papa",
  "glStartAt": "2025-01-01"
}

Copies only the narrow bootstrap state:

  • tenant shell, listings, and owner identities (as contacts). A legacy owner company type of company has no VRT equivalent, so the contact migrates without a company type. VRT shows a contact without a company type as an individual, so set the tax classification on these contacts after the migration. Any other legacy company type that VRT does not support fails the operation with a USER_ERROR before any data is written, with details.ownerId and details.companyType;
  • connection shells — credentials stripped and disabled = true. Credentials never move from the VRI team to the VRT team: the customer reconnects the PMS on the VRT team, and the VRI connection is set inactive. A shell keeps status = active, but the VRPilot connection steps (PMS, payment processors, Airbnb, Ramp, workflows) ignore disabled connections. They complete only after the customer reconnects;
  • derived ownership periods, preferring historical statement-owner snapshots from glStartAt forward and falling back to current listing owners. Owners without a split share equally. The first ownership starts at inception, and the GL cutover does not split unchanged ownership.

Inactive legacy listings with no PMS reference, no statements and no reservations are not copied. Opening balances, bank accounts and bank connections are never copied. After bootstrap, enter an opening trial balance and connect banks on the VRT team.

Bootstrap copies no reservations. The customer reconnects the PMS on the VRT team, and its first fetch adopts the placeholder reservations that the historical statement import creates for the stays on imported statements.

Bootstrap finishes by initializing the target team's chart of accounts, statement layouts, and recurring fees from the default template, so the mapping previews below work immediately. It also converts legacy management commission rates and listing assignments into inactive VRT fee drafts. A percentage such as 17.5 becomes 17,500 basis points, while a flat amount such as 49.95 becomes 4,995 cents. Listing-specific rates, exclusions, and the later of the legacy start date or GL cutover are preserved. A rate set on a listing collection applies to its listings unless the listing has its own rate. Listings with an effective zero rate, an exclusion, or a zero formula on the listing or its collection remain unsubscribed. A zero formula or zero rate on a booking channel becomes a !<channel uniqueRef> entry in the draft's bookingChannelsFilter. A channel-specific rate other than the default rate, and any non-zero formula on a listing, collection or channel, has no VRT equivalent: the draft is still created and its migration review record lists channelRate or unsupportedFormula. A setting whose target is not a source listing, collection or booking channel is listed as unresolvedTarget. A rerun of the bootstrap keeps existing drafts unchanged, including their channel filter.

Percentage drafts use the default VRT management-fee formula and posting accounts. Flat drafts start with a zero formula and separate inactive posting accounts. Their migration review record flags the formula and posting accounts for review. Replace those fields with the source fee's intended charge and accounts before activation. Review revenue recognition on every draft because VRI provider-item formulas and invoice-derived dates do not have an exact VRT equivalent. Automations with invalid rate settings or start dates do not create a draft. Tax rates and line mappings are not part of initialization; run POST /team/init with the target team in x-team-id if the team needs them. No statements are imported yet. That is the follow-up flow below.

Assisted direct-team preview

The direct-team migration pilot uses an internal preview operation rather than adding preview behavior to the Partner API contract. It creates the same deterministic GL target once, copies only the selected active admin and user memberships, and records a snapshot timestamp. One VRI source can have only one preview, and normal bootstrap cannot replay over it. The target remains disconnected: connection shells have no credentials, fetching is disabled, and preview creation or later team edits do not queue billing or new-customer lifecycle effects. Preparation enables the preview feature for both teams so the selected users see the source invitation and target preview banner.

The preview copies no reservations. The customer reconnects the PMS on the VRT team, and its first fetch creates them. Repeating the exact preparation request returns the existing target without replaying bootstrap; requests that change the target identity or selected users are rejected.

After the customer and operator finish setup, the internal activation operation requires confirm=true. It marks the existing preview active and queues the normal billing, CRM, and analytics effects. Retrying activation repairs missing effects. Activation moves no credentials: the customer reconnects the PMS on the VRT team and the VRI connection is set inactive. Customer changes in the target are preserved; activation never recreates the team. The source team keeps a migration banner that links to the live target after activation.

Preparation and activation are recorded by server-owned lifecycle events only after their writes complete. Migration verification is a separate transition: the authoritative migration observer emits it through the shared typed tracking client only after the active preview retains its source/target link, every target PMS is enabled, and the first target fetch has completed successfully. The source team may remain active for its redirect banner. Retries reuse the original transition identity, and lifecycle events contain only the team pair, lifecycle timestamps, and bounded lifecycle or verification status.

Review live reservation mapping setup

GET /partner/vri-to-vrt/reservation-line-mappings
  ?sourceTeamId=...&targetTeamId=...&limit=100&offset=0

This read exposes sanitized original VRI setup for a linked non-GL source and GL target. It accepts a Crunchy source and a US target. Both teams must belong to the selected partner in the control plane and their current regional records. Frozen or migrated-away copies are rejected. The target needs its migration source link and GL start date. The route uses the partner's normal API region, then opens only the authorized source and target regions. Bootstrap and historical-statement routes retain their existing region restrictions.

The route requires reservation-mappings:read. Partner API keys derive the partner identity from the key. OAuth callers select the partner team, with the equivalent read permission. Source and target ids remain query parameters.

The response keeps source facts separate from native VRT configuration:

FieldMeaning
sourceTeam, targetTeamVerified team id, name, and dataRegion.
lineTypesObserved or configured source line type and nullable appId.
rulesOriginal stored rules, including unsupported rows with withheld values.
total, hasMoreRule count and whether more rules exist.
hasMoreLineTypesWhether more source line types exist.

Each rule returns its id, supported key, status, updatedAt, automationId, localAutomationId, parentSettingId, and both connection ids. left and right preserve the stored type and supported value orientation. A referenced provider account, item, or tax record appears as source with id, type, remoteId, name, status, isArchived, and classification. No credentials, raw settings, or provider payloads are returned.

isDefault identifies a supported wildcard. inclusion returns a supported explicit exclude, invoice, or journalEntry choice. A missing rule or null inclusion remains unresolved. Child rules retain their parent reference and condition; the response does not evaluate which rule wins.

Every rule has manualReviewRequired. Other flags identify unsupportedKey, unsupportedType, unsupportedValue, missingSourceReference, archivedSourceReference, inactiveSourceReference, orphanChild, or providerItemRequiresReview. Unsupported values are withheld. A missing reference may have been deleted, but the response does not prove deletion. Provider formulas, tax, party, and recognition rules may need a human choice.

Increase offset by limit until both pagination flags are false. Rules and line types paginate independently. Read current VRT accounts and reservation line mappings separately with the target team selected, including inactive accounts and all pages. Historical-statement account mappings describe a different accounting flow and cannot establish live reservation mappings.

The custom Claude MCP connector exposes the guide resource vrplatform://guides/vri-reservation-mappings and prompt vrt-vri-reservation-mapping-review. Use vrt_api_read for this route and the native target reads. Present a read-only comparison with explicit target account ids, exclusions, booking-channel exceptions, and unresolved facts. Never silently choose an accounting account by name or reuse a provider id as a VRT account id.

After explicit human review, re-read current target mappings and use the existing native POST /reservations/line-mappings to create a type or PUT /reservations/line-mappings/{type} to update it. Custom MCP calls use vrt_api_mutation with the target teamId and confirm=true. A null accountId excludes the type. A supplied channelMappings array replaces the existing exceptions, so preserve unedited entries. These writes keep normal permissions, accounting locks, journal refreshes, and audit events.

The setup read changes no stored configuration and emits no business audit event. The directory MCP client cannot call the migration or reservation mapping routes. This flow neither translates rules automatically nor activates the target.

2. Review account mappings

GET /partner/vri-to-vrt/statements/account-mappings
  ?sourceTeamId=...&targetTeamId=...

Returns the legacy accounts that the current migration plan would import, with a suggestion per account and the target account pool:

{
  "effectiveGlStartAt": "2025-02-01",
  "oldAccounts": [
    {
      "legacyAccount": {
        "key": "legacy-ref:341",
        "ref": "341",
        "name": "Rental Income",
        "classification": "Revenue"
      },
      "sources": ["line", "template"],
      "suggestedTargetAccount": { "id": "rents-uuid", "title": "Rents" },
      "currentTargetAccount": null,
      "lineDescriptionOverridesAllowed": false,
      "currentLineDescriptionOverrides": []
    }
  ],
  "unmatchedAccounts": [
    {
      "legacyAccount": {
        "key": "legacy-path:str liabilities owner payables net earnings rents",
        "name": "STR Liabilities:Owner Payables:Net Earnings:Rents"
      },
      "statementCount": 226,
      "firstStatementStartAt": "2024-01-01",
      "lastStatementStartAt": "2024-06-01"
    }
  ],
  "newAccounts": [
    {
      "id": "rents-uuid",
      "title": "Rents",
      "category": {
        "id": "category-uuid",
        "name": "Revenue",
        "classification": "revenue"
      },
      "type": "ledger",
      "status": "active"
    }
  ]
}
  • suggestedTargetAccount is the built-in mapper's pick. null means the account needs an explicit user choice, and import rejects the request while any preview key remains unresolved. The mapper names guest revenue accounts without the Guest Revenue: prefix of the default chart, so Rents suggests Guest Revenue: Rents unless an account with the exact short title exists.
  • currentTargetAccount reflects mappings stored by a previous import run, so reopening the mapping screen shows what is already resolved.
  • A legacy-path: key appears when line metadata preserves a structured account path that is more specific than its linked source account. All lines with the same normalized path share one mapping, regardless of their descriptions.
  • lineDescriptionOverridesAllowed is true only for a genuinely broad path, such as Owner Expenses, where the path alone cannot identify one target. Previously stored override rules are returned in currentLineDescriptionOverrides.
  • newAccounts includes active revenue, expense, and liability ledger accounts that can be selected as explicit targets.
  • Both lists can exceed a client's response cap. Send limit (1 to 200) and offset; both oldAccounts and newAccounts are sliced by the same window. Follow offset until hasMore is false. oldAccountsTotal and newAccountsTotal give the full counts. Without limit every account is returned.
  • Only legacy accounts on imported, non-zero, non-payout statement lines are listed; template-only accounts are skipped.
  • unmatchedAccounts lists account names on those lines that match no legacy account, with the number of statements that have lines under the name and the first and last statement month. A line keeps the name it was posted under, so a renamed account shows here: lines posted before the rename under Rents do not match the account now called STR Rents. Names match by their full text only. A parent or last segment of the name is never used. The list is returned unsliced on every page. See Unmatched accounts.

3. Review listing mappings

GET /partner/vri-to-vrt/statements/listing-mappings
  ?sourceTeamId=...&targetTeamId=...

Returns legacy listings that own importable historical statements or own a reservation referenced by those statements, exact uniqueRef target suggestions, and the target listing pool:

{
  "effectiveGlStartAt": "2025-02-01",
  "oldListings": [
    {
      "legacyListing": {
        "id": "legacy-listing-uuid",
        "name": "Ravensong",
        "uniqueRef": "461898",
        "status": "active",
        "pmsStatus": "active"
      },
      "importedStatementCount": 12,
      "referencedStatementCount": 14,
      "suggestedTargetListing": {
        "id": "target-listing-uuid",
        "name": "Ravensong",
        "uniqueRef": "461898",
        "status": "active",
        "pmsStatus": "active"
      },
      "currentTargetListing": null,
      "currentMapping": null
    }
  ],
  "newListings": [
    {
      "id": "target-listing-uuid",
      "name": "Ravensong",
      "uniqueRef": "461898",
      "status": "active",
      "pmsStatus": "active"
    }
  ]
}
  • limit (1 to 200) and offset slice oldListings and newListings by the same window. Follow offset until hasMore is false; oldListingsTotal and newListingsTotal give the full counts. Without limit every listing is returned.
  • suggestedTargetListing is set only when exactly one target listing has the same uniqueRef as the legacy listing.
  • importedStatementCount counts importable statements that belong to the legacy listing.
  • referencedStatementCount also includes statements that reference a reservation on the legacy listing. It can be higher than importedStatementCount, but those extra statements belong to another listing and do not create statements on this listing.
  • currentTargetListing and currentMapping show what a previous import run stored.
  • currentMapping = "historicalListing" means the source listing was imported as an inactive historical-only listing.

Re-listed properties

Treat a re-list as continuity of the same physical property. Send an explicit mapping from every old source listing id to the current live target listing id, even when their names or uniqueRef values differ. Multiple old source listings may map to the same current target:

{
  "listingMappings": [
    {
      "sourceListingId": "old-listing-a-uuid",
      "targetListingId": "current-live-listing-uuid"
    },
    {
      "sourceListingId": "old-listing-b-uuid",
      "targetListingId": "current-live-listing-uuid"
    }
  ]
}

The import reuses the target listing's native ownership period active at the cutover and attaches the historical statements to that timeline. Use targetListingId: null only when the source listing has no live VRT counterpart; null intentionally creates a separate inactive historical-only listing.

If multiple mapped source listings contain statements with the same start date, the import creates one target statement for that listing and date. It combines their balances and totals and retains every eligible source line. This keeps re-listed history complete without creating duplicate target statements.

4. Import historical statements

POST /partner/vri-to-vrt/statements
{
  "sourceTeamId": "legacy-team-uuid",
  "targetTeamId": "migrated-team-uuid",
  "accountMappings": [
    {
      "legacyAccountKey": "legacy-ref:341",
      "targetAccountId": "rents-uuid"
    }
  ],
  "lineDescriptionOverrides": [
    {
      "legacyAccountKey": "legacy-path:trust liabilities owner expenses",
      "descriptionIncludes": "propane",
      "targetAccountId": "utilities-uuid"
    }
  ],
  "legacyAccountAliases": [
    {
      "legacyAccountKey": "legacy-path:str liabilities owner payables net earnings rents",
      "sourceAccountRef": "133"
    }
  ],
  "excludedLegacyAccountKeys": ["legacy-name:damage waiver revenue"],
  "listingMappings": [
    {
      "sourceListingId": "legacy-listing-uuid",
      "targetListingId": "target-listing-uuid"
    },
    {
      "sourceListingId": "legacy-listing-without-vrt-match-uuid",
      "targetListingId": null
    }
  ]
}

The response acknowledges the asynchronous job:

{
  "operationId": "750c9ce8-81ed-41b9-b04e-7f9330c583ed",
  "status": "queued"
}
  • statementLayoutId optionally selects a native layout owned by the target team for its first historical import. The import uses the layout configuration present when the queued operation executes and stores it on each imported statement. Every direct or formula account reference must belong to the target chart of accounts.
  • Omit statementLayoutId to keep the existing default-layout import and rerun behavior. A later operation with an explicit selection fails with historicalImportExists, even after imported statements are deleted.
  • Each accountMappings[] entry binds one oldAccounts[].legacyAccount.key from the preview to one target account id. Explicit mappings normally win over the built-in mapper. Every preview key must resolve before import.
  • Every unmatchedAccounts[] key needs one decision before import: legacyAccountAliases[] reads its lines as the legacy account with the given sourceAccountRef, excludedLegacyAccountKeys[] leaves its lines out. An open key returns 400 with context.legacyAccountKeys and queues nothing. An alias that names an unknown reference, or an exclusion that names a key that is not unmatched, also returns 400. Both lists are stored on the target team.
  • The preview does not show an aliased key or its suggested target account. Send an explicit accountMappings[] entry for every aliased key, so the target account is chosen and not taken from the built-in suggestion.
  • lineDescriptionOverrides[] is optional and accepted only when the matching preview row has lineDescriptionOverridesAllowed: true. Rules use normalized substring matching, with the most specific matching substring winning.
  • The endpoint checks the account and listing mappings, then durably queues the import. Poll GET /operations/{id} with the returned operationId until it reaches completed or failed; see Asynchronous operations for the complete status and timestamp contract.
  • With MCP, call vrt_api_read with path: /operations/<operationId>. API-key operation reads preserve the credential's Partner tenant and do not require a teamId. When the target is homed outside the partner's region, the operation lives in the target's region. With the partner as teamId the status is readable within about 5 minutes; send x-data-region set to the target's region (dataRegion in MCP) to read it at once. You can also read the target team's historicalStatementsImportedAt with GET /team and the target teamId.
  • Resolved mappings are stored on the target team, so the import can be rerun and the preview returns them as currentTargetAccount.
  • listingMappings[] works the same way for listings: one oldListings[].legacyListing.id from the listing preview per entry, bound to one target listing id or to null for an inactive historical-only listing.
  • Exact uniqueRef listing suggestions are applied when no explicit listing mapping is sent for that source listing.
  • Every preview row without an exact suggestion requires an explicit mapping. Omitting one returns 400 with the source listing ids in context.sourceListingIds and queues nothing; use targetListingId: null for an inactive historical-only listing.
  • The legacy source may live in any region, usually crunchy. The import writes into the target's region, so the API places POST /partner/vri-to-vrt/statements by targetTeamId in the body, whatever x-team-id or the Partner API key says. The gateway api.vrplatform.app forwards it to the target's region. A regional API that serves another region returns 421 with the target's dataRegion and apiBaseUrl before anything is queued. A target team copy that was moved to another region fails before any historical statement is written.
  • A migrated-away source team copy is also rejected. If the source has no statement that precedes the effective cutover, including a source with no statements at all, the operation fails without marking the historical import complete.
  • Every statement listing and referenced reservation listing must resolve before the import writes reservations. An unresolved reservation listing returns a user error instead of a database foreign-key failure.
  • A historical statement always uses an ownership period from its own mapped target listing. Reservations referenced by that statement can remain on a different mapped listing without moving or duplicating the statement. If no ownership period can be derived, the operation fails with a controlled error instead of silently dropping the statement.
  • The import brings over pre-cutover owner statements, their provenance rows, attached historical journal entries, and statement templates rewritten onto the target chart. Duplicate legacy rows for one listing and period resolve to the most recently updated source row, with the source id breaking ties.
  • Imported statement totals are derived from the same eligible source rows and owner-transfer lines as their historical journal entries. Stored legacy totals are used only when no eligible rows exist, and owner contributions preserve their direction.
  • Zero-value source lines do not require account mappings, affect imported reservation totals, or create historical journal rows.
  • Explicit mappings onto existing VRT listings reuse the native ownership period active at cutover. This keeps migrated history and native statements on one ownership timeline when the owners did not change.
  • When many-to-one listing mappings produce multiple source statements for the same target listing and start date, the import consolidates them into one target statement while retaining all derived totals and eligible lines.
  • Imported reservations already referenced by live journal entries are updated in place during a rerun. Their ids and journal links remain stable while the corrected listing mapping is applied.
  • Reservation reuse checks both (connectionId, uniqueRef) and PostgreSQL's active-row deduplication identity (connectionId, listingId, confirmationCode) before insertion. Statement lines from every matching source reservation attach to the shared target row.
  • On success, the target team's statementStartAt moves to the effective cutover and historicalStatementsImportedAt records the run.
  • Historical reservations with checkOut <= checkIn import with no checkOut and zero nights. Imported statement and journal currencies are lowercase.
  • The import preserves booksClosedAt and can write the historical ledger without reopening the live books. If separate remediation really requires changing the hard accounting lock, call PUT /team/books-closing with a token carrying partner:general-ledger:v1, then restore the boundary after the remediation.

Repair an existing migration

POST /partner/vri-to-vrt/booking-channels
{
  "sourceTeamId": "legacy-team-uuid"
}

This idempotent backfill reads only classified booking channels used by the source team's reservations, publishes them to the control plane, and queues every-region projections. It does not rerun team bootstrap or import historical statements. The response returns bookingChannelCount. The route requires the partner:provisioning:v1 bundle.

Exact contracts

The migration routes are part of the generated Partner API reference under the Partner / VRI Migration tag. The request and response schemas there are the exact contracts; this guide explains how to sequence them.

Copy selected customer uploads

The operator script scripts/ben-2/pro-19096-copy-files.ts uses existing Files APIs. It defaults to a metadata-only inventory of explicitly selected source IDs. The inventory returns IDs and classifications without filenames, storage keys, owner details or contents. Missing legacy URLs do not establish missing objects.

Use an authenticated user with file read access on the source and file read/write access on the target. Both teams must be an authorized linked pair in their authoritative regions. Completed customer uploads need an existing target owner contact resolved through migration identity. Missing or ambiguous mappings exclude the file. Generated files and provider payloads are excluded. Copying retains all VRI metadata and objects and preserves existing owner-access rules.

bun --env-file=/absolute/path/to/resolved/.env scripts/ben-2/pro-19096-copy-files.ts \
  --source <vri-team-id> --target <vrt-team-id> \
  --source-file-id <selected-file-id> --auth-file /private/auth.json

The private auth file contains the operator's existing accessToken. Repeat --source-file-id for each selected file, up to 100 per invocation. Only an explicitly authorized customer execution may add --copy. Supply --checkpoint /private/copy.json and --new-checkpoint for the first copy invocation. Keep that private checkpoint for later selections and retries. Omit --new-checkpoint when resuming.

The script saves the returned fileId and uploadRef before PUT. A lost POST response before PUT creates no copied file. A lost PUT response reuses the saved reference. Ready target files are recognized without recopying. Expired references, missing checkpoints and unavailable targets stop their affected work for explicit operator recovery. References are not renewed automatically. Deleted targets are not recreated. Files retain the existing size/type checks and upload size limit, without a cryptographic integrity guarantee. Existing Files API audit rules apply.

On this page