VRPlatformVRPlatform
Integrate & Migrate Data

Historical Statements

Import pre-platform owner statement history from any source through the API

When a team joins VRPlatform, its owners usually have months or years of statement history in the previous system — spreadsheets, PDFs, or a legacy platform. Importing that history preserves the exact owner-facing snapshots; the listing opening balance separately establishes the first live statement.

POST /statements/historical accepts already-resolved statement data from any client. The API does not parse files — your client code extracts the data (from CSV, PDF, an old database, anything) and resolves listings and accounts via the existing endpoints; the API turns each payload into a real owner statement with journal history.

This is the same mechanism the VRI migration uses for its historical statement import.

Concepts

The historical ledger

Imported statement lines become journal entries with ledger = historical and status = inactive. They render on owner statement details and feed owner-facing balances, but they never mix into the live general ledger — trial balances and GL reports are unaffected.

Rendering by account

A historic statement has no stored row layout. The statement detail maps its journal entries onto the team's statement layout by account, the same way live statements render. So the import payload references real chart accounts (accountId), and your client decides which account each source line belongs to — typically with a mapping table the user confirms once, like the VRI account mappings.

Reservation rows

A line can carry either an optional reservationId or matchReservationConfirmationCode. Reservation-linked lines render as reservation rows on the statement detail instead of anonymous one-off lines; guest name, check-in/check-out, and confirmation code come from the referenced reservation.

The reservation must already exist in the team: either synced from a PMS connection or created through the reservation endpoints before posting the statement. Use reservationId when the client already has VRPlatform's UUID. Use matchReservationConfirmationCode when source files expose a PMS or channel booking code. Confirmation-code matching is limited to reservations on the statement's listingId, and unknown or ambiguous matches are rejected. When both references are supplied, they must resolve to the same reservation.

This endpoint never creates reservations. Lines without either reference are perfectly fine — they render as plain statement lines.

Idempotency

Both the statement and each line carry a client-chosen uniqueRef. Re-posting a statement uniqueRef replaces the previous import (the statement row is updated and its journal entries are deleted and recreated), so imports can be re-run safely while the user iterates on mappings. The retained import source also lets the same payload recover a statement whose historical rows were detached or whose statement row was deleted.

Each line uniqueRef combines with its optional reservation link to identify the imported journal line. For an unlinked line, the uniqueRef must be unique within the team. Re-posting the same statement can reuse its line references, but a different statement cannot claim the same journal key. The API rejects that conflict with the existing statement and journal entry IDs in the error context.

Prerequisites

  1. The listing exists and has an ownership period covering the statement range (with members, when payout lines are imported).
  2. The chart of accounts contains the accounts your lines reference, and a payout_distribution account exists when account-less payout lines are imported.
  3. A statement layout exists (the listing's assigned layout or the default is frozen onto the statement).

Import a statement

POST /statements/historical
{
  "uniqueRef": "csv-import:2025-03:ocean-view",
  "listingId": "listing-uuid",
  "startAt": "2025-03-01",
  "endAt": "2025-04-01",
  "currency": "usd",
  "status": "published",
  "centBalanceStart": 0,
  "lines": [
    {
      "uniqueRef": "csv-import:2025-03:ocean-view:rent",
      "type": "line",
      "accountId": "rents-account-uuid",
      "centTotal": 80000,
      "description": "March rent - Airbnb #HMNZ8WJ8TP",
      "matchReservationConfirmationCode": "HMNZ8WJ8TP",
      "date": "2025-03-15"
    },
    {
      "uniqueRef": "csv-import:2025-03:ocean-view:cleaning",
      "type": "line",
      "accountId": "housekeeping-account-uuid",
      "centTotal": -20000,
      "description": "Cleaning"
    },
    {
      "uniqueRef": "csv-import:2025-03:ocean-view:payout",
      "type": "payout",
      "centTotal": 50000
    }
  ]
}

When omitted, status defaults to published, centBalanceStart defaults to 0, and an ordinary line's type defaults to line. A line requires an accountId. A line that references the team's payout_distribution account is classified as a signed transfer while retaining that account and its optional contact or reservation attribution. A payout omits the account and uses the payout-distribution assignment.

Amounts are owner-facing: revenue is positive and expenses are negative. A payout amount is positive when paid to the owner. A line on the payout-distribution account is already signed for the statement: negative pays the owner and positive reverses a payout. The statement financials are derived from the lines — here net income 600.00, payouts -500.00, balance end 100.00 — and centBalanceEnd can be supplied explicitly when the source data carries its own balances.

Payout lines without a contactId are split across the ownership period members by their ownership share, one journal entry per member.

The response reports what was created. Note the sign flip on the payout: the payload uses owner-facing signs (payout +50000), while the response financials use statement-side signs, where transfers to the owner are negative (centTransfer: -50000):

{
  "id": "statement-uuid",
  "uniqueRef": "csv-import:2025-03:ocean-view",
  "status": "published",
  "replaced": false,
  "journalEntryCount": 3,
  "financials": {
    "centBalanceStart": 0,
    "centBalanceEnd": 10000,
    "centTotal": 60000,
    "centExpenses": -20000,
    "centNetRevenue": 80000,
    "centTransfer": -50000
  }
}

Guardrails

  • The statement range must be covered by one listing ownership period; statements overlapping an existing statement on the same period are rejected.
  • A uniqueRef belonging to a statement that was not created by this import is rejected — live statements can never be overwritten.
  • A line journal key already owned by another imported statement is rejected.
  • Imported snapshots use inReview or published. They cannot move to draft; re-post the same statement uniqueRef to correct imported data.
  • Deleting an imported statement — DELETE /statements/{id} with onLocked=unlockAndDelete also removes its historical journal entries.

Cutover balances

Historical statements chain through their imported centBalanceStart and centBalanceEnd. Import months oldest-first and preserve the source balances.

Configure each listing opening balance for the first live statement. A final historical snapshot may end on statementStartAt even though the opening balance journal posts on the preceding day: the imported snapshot keeps its stored figures and does not consume that live opening-balance journal.

An existing listing opening balance can cross stored imported history when every stored statement for that listing is an imported snapshot ending at or before statementStartAt. Books closing and direct statement attachments still block the write. Once an inReview or published live statement exists at or after the cutover, its opening balance is locked.

API Reference

On this page