Migrate from Xero / MYOB / QuickBooks¶
Bringing an existing set of books across to SAE Books. The mechanics are the same regardless of the source system — only the export format differs.
Cutover date
Pick a clean cutover. End-of-quarter (after BAS) is best; end-of-month is fine. Mid-period migration works but doubles the reconciliation work because you're crossing two systems' GST accruals.
What you migrate vs. what you reset¶
| Migrate | Reset (start fresh in SAE Books) |
|---|---|
| Chart of accounts | Bank-feed connections (re-consent in SAE Books) |
| Customers / vendors / contacts | User accounts (recreate via signup or invite) |
| Open invoices and bills | API integrations (re-issue tokens) |
| Open credit notes | |
| Asset register | |
| Trial balance as at cutover |
You do not migrate historical transactions. They stay in the incumbent system as a read-only archive — Australian tax law requires five years of retention regardless of which platform you're running today.
1. Export from the incumbent¶
| Source | Export endpoint / report |
|---|---|
| Xero | Reports → Export, plus Reports → Trial Balance for the cutover snapshot |
| MYOB | File → Export Data |
| QuickBooks Online | Reports → Export to Excel; Lists → Customer/Vendor → Export |
Get four CSVs at minimum:
- Chart of accounts (
code, name, type, parent_code) - Customers (
name, abn, email, billing_address, …) - Vendors (same shape, separate file)
- Trial balance as at cutover date
Plus, if you carry assets:
- Asset register (required columns:
code, name, purchase_date, cost, depreciation_model_id, cost_account_code, accum_dep_account_code)
2. Transform¶
SAE Books provides a QBO CSV import parser (saebooks/services/imports/qbo.py) that handles
QBO's standard "Customer.csv" / "Vendor.csv" and Account List CSV exports, mapping them to
SAE Books contacts and chart of accounts. For Xero and MYOB exports, you'll need to normalise
the CSV columns to match the SAE Books expected headers before upload — the column names vary
by source, but the CoA wizard accepts code, name, type, parent_code and the contact
wizard accepts name, email, phone, abn as core columns.
3. Import via the wizard API¶
All imports go through the wizard API at /api/v1/imports/wizards. There is no single-step
bulk endpoint that returns a job_id — the wizard is a multi-step flow: start, upload, preview,
commit.
# 1. Start a wizard (kind: "coa" for chart of accounts, "qbo" for QBO contacts/CoA)
curl -X POST "$SAEBOOKS_API_BASE/imports/wizards" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{"kind": "coa"}'
# → {"wizard_id": "...", "step": 0, "state": {}}
# 2. Upload the CSV (step patch)
curl -X POST "$SAEBOOKS_API_BASE/imports/wizards/$WIZARD_ID/step" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d "{\"step\": 0, \"patch\": {\"raw\": \"$(cat accounts.csv | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))')\"}}"
# 3. Commit
curl -X POST "$SAEBOOKS_API_BASE/imports/wizards/$WIZARD_ID/commit" \
-H "Authorization: Bearer $JWT"
# → {"new": N, "changed": M, "archived": K}
Available kind values: bank_csv, bank_ofx, coa, bill_csv (community tier);
qbo (Pro+ — requires FLAG_QBO_IMPORT).
The asset register import uses the web UI (Settings → Assets → Import CSV) rather than the JSON API — the flow is a two-step preview-then-apply form submission. Use the UI for asset migrations.
4. Opening balances¶
The trial-balance snapshot from your incumbent becomes a single opening-balances journal in SAE Books — same pattern as a first chart of accounts. The journal date is the day after your cutover (so the incumbent owns up to and including the cutover date, SAE Books owns from the next day).
5. Open AR / AP¶
Open invoices and bills are re-entered as live documents, not history. There is no
bulk JSON endpoint for invoices — create them individually via POST /api/v1/invoices
(one per open invoice), or use the bill_csv import wizard for bills:
# Individual invoice
curl -X POST "$SAEBOOKS_API_BASE/invoices" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d @invoice.json
# Bills in bulk — use the bill_csv wizard (kind: "bill_csv")
# Bills are imported as DRAFTS for human review before posting.
Each invoice posts the same journal it would have if you'd raised it natively, minus any payments — those land via the bank feed once you reconnect.
6. Reconcile¶
Run the trial balance in both systems and compare. They should agree to the cent. Any difference is one of:
- A miscoded transformation (foreign currency line, GST flag, account-type confusion) — fix the script and re-run
- An incumbent-system quirk (Xero's "system accounts", MYOB's job-cost accruals) — usually resolved by re-mapping to the closest SAE Books equivalent
- A genuinely missing entry — post it manually as an opening-balance adjustment, with a clear narration
7. Cut over¶
Stop posting in the incumbent. Direct bank feeds, integrations, and users at SAE Books. Keep the incumbent in read-only archive mode for the legal retention period.
Next¶
- Connect a bank feed — the next thing you'll want is fresh data flowing in
- File your first BAS — the first BAS after cutover proves the migration was clean