Skip to main content
All Docs
FeaturesPurple PepperUpdated April 7, 2026

Data Migration — Bulk CSV Import

Data Migration — Bulk CSV Import

The data migration module lets organisation owners and admins bulk-import landlord and property records from CSV files. It is designed for one-time or periodic migrations from legacy systems.

Access: Owner and admin roles only. This feature is not available to standard members.


Accessing the Module

Navigate to Dashboard → Admin → Data Migration (/dashboard/admin/data-migration).

The page shows:

  • Summary cards — total migrated landlords, total migrated properties, and overall job count
  • Import wizard — create a new import job in three steps
  • Import history — all past jobs with status, row counts, and expandable error details

Import Wizard

The wizard guides you through three steps:

Step 1 — Select Type

Choose whether you are importing Landlords or Properties.

Step 2 — Upload CSV

  1. Enter an Import Label to identify this job (e.g. Legacy Landlords from Xero - Jan 2025).
  2. Review the Expected CSV Columns panel — required columns are highlighted in red.
  3. Upload your .csv file using the file picker or drag-and-drop.
  4. If you need a reference file, click Download Sample CSV to get a pre-formatted example.
  5. Click Validate to submit the file for parsing and validation.

Step 3 — Review & Import

After validation you will see:

  • Total rows, valid rows, and error rows found in the file
  • Detected columns from your CSV header row
  • Validation errors (up to 20 shown) with row numbers and field names

If there are errors you can go back, fix your CSV, and re-upload. If the file is acceptable, click Run Import to execute.


CSV Format

Landlords

ColumnRequiredNotes
first_name
last_name
emailMust be a valid email address
phone
landlord_typeEnum value (e.g. individual, company)
address_line_1
city
postcode

Sample:

first_name,last_name,email,phone,landlord_type,address_line_1,city,postcode
John,Smith,john.smith@example.com,07700900000,individual,10 Downing Street,London,SW1A 2AA
Jane,Doe,jane.doe@example.com,07700900001,company,1 Business Park,Manchester,M1 1AA

Properties

ColumnRequiredNotes
address_line_1
city
postcode
property_typeEnum value (e.g. single_let, hmo)
bedroomsNumeric
bathroomsNumeric
landlord_emailLinks property to an existing landlord

Sample:

address_line_1,city,postcode,property_type,bedrooms,bathrooms,landlord_email
10 High Street,London,E1 6AN,single_let,2,1,john.smith@example.com
5 Park Road,Manchester,M1 4BH,hmo,6,3,jane.doe@example.com

CSV formatting notes:

  • Fields containing commas must be wrapped in double quotes
  • BOM characters (common in Excel exports) are handled automatically
  • Files must use UTF-8 encoding

Duplicate Handling

  • Landlords: Rows with an email address already associated with a landlord in the same organisation are silently skipped — they are not counted as errors.
  • Properties: No duplicate detection by default; each row creates a new property record.

Error Handling

  • Up to 500 validation or import errors are stored per job.
  • The first 20 errors are surfaced in the wizard review step and in the job detail view.
  • Each error includes the row number, field name, and a human-readable message.
  • Import-time failures are captured for debugging.

Job Statuses

StatusMeaning
pendingJob created, not yet processed
validatingCSV is being parsed
validatedValidation complete, awaiting execution
importingImport in progress
completedAll processable rows imported
failedJob encountered a fatal error

Audit Trail

Every successful migration leaves a verifiable audit trail:

  • Imported landlord and property records are flagged with isMigrated = true.
  • Each record stores the migrationJobId it was created by.
  • Audit log entries are written for the data_migration.validated and data_migration.completed events.

tRPC API Reference

All endpoints are under the dataMigration router and require admin or owner permissions.

dataMigration.getColumnSpec

Returns the required and optional column names for a given import type.

Input:

{ importType: "landlords" | "properties" }

Returns:

{ requiredColumns: string[], optionalColumns: string[] }

dataMigration.validate

Parses and validates a raw CSV string. Creates a job record in validated status.

Input:

{
  importType: "landlords" | "properties",
  label: string,
  csvData: string
}

Returns:

{
  jobId: string,
  status: string,
  totalRows: number,
  validRows: number,
  errorRows: number,
  hasErrors: boolean,
  detectedColumns: string[],
  sampleErrors: Array<{ rowNumber: number, field: string | null, message: string }>
}

dataMigration.execute

Imports validated rows from a job into the database.

Input:

{ jobId: string }

dataMigration.list

Lists migration jobs for the organisation.

Input:

{ limit?: number, importType?: "landlords" | "properties" }

dataMigration.get

Returns a single job and all its associated errors.

Input:

{ jobId: string }

dataMigration.summary

Returns aggregate migration statistics.

Returns:

{
  totalMigratedLandlords: number,
  totalMigratedProperties: number,
  jobStats: Record<string, number>  // status → count
}