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
- The listing exists and has an ownership period covering the statement range (with members, when payout lines are imported).
- The chart of accounts contains the accounts your lines reference, and a
payout_distributionaccount exists when account-less payout lines are imported. - 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
uniqueRefbelonging 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
inRevieworpublished. They cannot move todraft; re-post the same statementuniqueRefto correct imported data. - Deleting an imported statement —
DELETE /statements/{id}withonLocked=unlockAndDeletealso 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
- Import a historical statement — POST /statements/historical
- Delete an imported statement —
DELETE /statements/{id}
