Migrating with import bundles
A migration is not many independent imports. Leases reference units, units reference properties, payments reference charges — import them separately and you get orphans.
An import bundle takes every CSV at once, resolves the relationships between them, shows you what it intends to do, and only then writes.
create ──> upload files ──> validate ──> review candidates ──> start ──> resultsNothing is written to your portfolio until start.
1. Get the templates
Section titled “1. Get the templates”curl "https://api.getvespy.com/api/import-bundles/templates/appfolio" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ --output vespy-templates.zipprovider is generic, appfolio, or turbotenant. The provider-specific templates
match the export column names of that system, so you can usually feed its export straight
in. Use generic for anything else.
2. Create the bundle
Section titled “2. Create the bundle”curl -X POST https://api.getvespy.com/api/import-bundles \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "name": "AppFolio migration — March 2026", "provider": "appfolio", "sourceNamespace": "appfolio-prod", "sourceAccountLabel": "Northside Property Group" }'sourceNamespace is what makes a migration re-runnable. External ids from the old system
are recorded under it, so re-importing an updated export updates the records it created
before rather than duplicating them. Keep it stable across runs of the same migration; use
a different one for a genuinely different source.
3. Upload the CSVs
Section titled “3. Upload the CSVs”curl -X POST "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/files" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -F 'file=@properties.csv' \ -F 'file=@units.csv' \ -F 'file=@tenants.csv' \ -F 'file=@leases.csv'Upload them all before validating. Column mapping is inferred; correct it with
PATCH /api/import-bundles/{id}/files/{fileId}, and save a mapping you will reuse with
POST /api/import-mapping-profiles.
4. Validate
Section titled “4. Validate”curl -X POST "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/validate" \ -H "Authorization: Bearer $ACCESS_TOKEN"Validation is a dry run. It resolves cross-file references, matches rows against records
you already have, and produces one candidate per row. The bundle moves through
inspecting → mapping → validating and lands on ready.
5. Review the candidates
Section titled “5. Review the candidates”curl "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/candidates?status=ambiguous" \ -H "Authorization: Bearer $ACCESS_TOKEN"| Candidate status | What it means |
|---|---|
ready_create |
Will be inserted |
ready_update |
Matched an existing record; will be updated |
unchanged |
Matched, nothing differs |
duplicate_merged |
Two source rows resolved to one record |
ambiguous |
Matched more than one record — needs your decision |
missing_source_data |
A required field is empty |
invalid |
Failed validation |
blocked |
A record it depends on is not importable |
excluded |
You chose to skip it |
Work through ambiguous and invalid first. Resolve a candidate — pick the right match,
or exclude it — with PATCH /api/import-bundles/{id}/candidates/{candidateId}.
blocked usually clears on its own once the record it depends on is fixed, since the
dependency is what was blocking it.
6. Commit
Section titled “6. Commit”curl -X POST "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/start" \ -H "Authorization: Bearer $ACCESS_TOKEN"The bundle goes queued → processing → completed, or completed_with_errors if some
rows failed while the rest were written. Poll GET /api/import-bundles/{id} for status, or
watch the event stream.
POST /api/import-bundles/{id}/cancel stops a bundle that has not started writing.
7. Reconcile
Section titled “7. Reconcile”curl "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/failures.csv" \ -H "Authorization: Bearer $ACCESS_TOKEN" --output failures.csv
curl "https://api.getvespy.com/api/import-bundles/$BUNDLE_ID/identity-map.csv" \ -H "Authorization: Bearer $ACCESS_TOKEN" --output identity-map.csvfailures.csv is the rows that did not import, with the reason on each — fix and re-upload
under the same sourceNamespace.
identity-map.csv maps each source id to the Vespy id it became. Keep it: it is how you
reconcile against the old system, and how anything still pointing at the old ids catches
up.