Skip to content

Business Central to Xero Migration: The CTO's Technical Guide (2026)

A technical guide to migrating from Business Central to Xero — covering API limits, dimension mapping, field-level data mapping, and step-by-step execution.

Raaj Raaj · · 22 min read
Business Central to Xero Migration: The CTO's Technical Guide (2026)
TALK TO AN ENGINEER

Planning a migration?

Get a free 30-min call with our engineers. We'll review your setup and map out a custom migration plan — no obligation.

Schedule a free call
  • 1,200+ migrations completed
  • Zero downtime guaranteed
  • Transparent, fixed pricing
  • Project success responsibility
  • Post-migration support included

Migrating from Microsoft Dynamics 365 Business Central to Xero is not an upgrade — it's a structural compression. You're moving from a multi-entity ERP with dimensional accounting, custom AL extensions, and deep supply chain modules into an SMB-focused cloud accounting platform with hard limits on tracking categories, invoice volumes, and API throughput.

Done correctly, it eliminates significant licensing overhead and operational complexity. Done wrong, you lose historical reporting granularity, trigger duplicate bank feed entries, or stall mid-migration when you hit Xero's 5,000 daily API call ceiling.

This guide covers the architectural mismatches you'll face, the real API constraints on both sides, object-by-object field mapping, a direct comparison of migration methods, and a step-by-step execution plan — so you can make an informed decision and execute with confidence.

For a related ERP-to-Xero migration with similar constraints, see our deep dive on Sage Intacct to Xero Migration: API Limits, Data Mapping & Methods.

Why Companies Downscale from Business Central to Xero

The most common drivers we see for this migration:

  • Cost reduction. Business Central licensing — Essential or Premium per-user CALs, Azure hosting, partner support contracts — runs significantly higher than Xero's flat monthly pricing. For a subsidiary or business unit that doesn't use manufacturing, warehousing, or advanced supply chain features, the savings are substantial.
  • Business unit spin-offs. A parent company retains Business Central for consolidated multi-entity reporting while a divested subsidiary moves to Xero for independent, simpler operations.
  • System simplification. Teams that never used BC's dimensional accounting, job costing, or project modules are paying for complexity they don't need. Xero is not suitable for all types of business, particularly those with very high transaction volumes. But for many mid-market companies that have simplified their operations, Xero's focused feature set is a better fit.
  • Ecosystem preference. Xero's app marketplace and native integrations with tools like Stripe, Shopify, and Gusto appeal to teams building a composable finance stack rather than relying on a monolithic ERP.
Warning

Before you commit: Xero has advisory monthly limits. Xero recommends the following monthly limits: 1,000 Sales Invoices, 1,000 Purchase Bills, 2,000 Account Transactions, 2,000 Bank Statement lines, 1,000 lines per Statement, and 10,000 Contacts. If your Business Central instance regularly exceeds these volumes, Xero may not be the right target. Audit your transaction volumes first.

Architectural Differences: ERP Dimensions vs. Xero Tracking Categories

This is the single biggest data model mismatch, and the place where most migrations lose fidelity.

Business Central uses a multi-dimensional ledger. You can set up two global dimensions and eight shortcut dimensions. You can actually create any number of dimension codes in the system, but only eight can be actively used on transaction entry screens at a time. Global dimensions are embedded directly into every G/L entry table, making them available as native filters across the entire system.

Xero uses tracking categories for analytical segmentation. Xero allows only 2 active tracking categories to be used within all transaction pages, including quotes, invoices, payments, bills, bill payments, and manual journals. Each tracking category supports approximately 100 options before performance degrades.

You're compressing an N-dimensional analytical model into a 2-dimensional one. Here's how to handle it:

Business Central Concept Xero Equivalent Compression Strategy
Global Dimension 1 (e.g., Department) Tracking Category 1 Direct 1:1 mapping
Global Dimension 2 (e.g., Cost Center) Tracking Category 2 Direct 1:1 mapping
Shortcut Dimensions 3–8 No direct equivalent Encode into Chart of Accounts structure, or archive in external data warehouse
Dimension Combinations Not supported Flatten to composite account codes
Analysis Views (up to 4 dimensions) Standard Xero reports (2 tracking categories max) Accept reduced granularity or use a BI layer
Info

Key decision: Before migration, your finance team must decide which two dimensions matter most for ongoing reporting. Everything else either gets encoded into the account code structure (e.g., 6010-SALES-US instead of a generic 6010 with a Region dimension) or archived in a data warehouse for historical queries. There is no workaround for Xero's 2-category hard limit.

Tip

Keep the full Business Central dimension detail in an external archive keyed by source document ID, dimension set ID, and target Xero document ID. That gives you a clean Xero file without destroying auditability.

Migration Approaches: CSVs, iPaaS, Custom ETL, and Managed Services

There are four viable paths. The right choice depends on your data volume, engineering capacity, and tolerance for manual cleanup.

1. Native CSV Export/Import (Xero Conversion Toolbox)

How it works: Export data from Business Central as CSV files (via built-in report exports or Configuration Packages), then import into Xero using the Conversion Toolbox or standard CSV import screens.

When to use it: Small businesses with a simple chart of accounts, under 500 invoices to import, and minimal historical data requirements.

Pros:

  • Zero cost for tooling
  • No API knowledge required
  • Works for Chart of Accounts, conversion balances, and contacts

Cons:

  • There's a practical limit of around 500 rows per import. For larger volumes, you need to split the file manually.
  • If any row has an error — a missing field, a bad date, an unrecognised tax code — Xero rejects the entire batch. You fix the error and re-upload the whole file.
  • No relationship preservation — invoice-to-payment links and contact-to-invoice associations must be rebuilt manually
  • Dimensions are lost entirely in CSV exports unless you pre-flatten them

Complexity: Low

For a broader perspective on CSV-based migrations, see Using CSVs for SaaS Data Migrations: Pros and Cons.

2. iPaaS / Middleware Platforms (Skyvia, Make, Zapier)

How it works: Use a cloud integration platform with pre-built connectors for both Business Central and Xero. Configure mapping rules in a visual editor, then run the data transfer.

When to use it: Mid-size migrations where you need some transformation logic but don't have the engineering bandwidth for custom scripts. Also suitable for ongoing sync scenarios.

Skyvia's integration is configured in convenient GUI wizards and editors. Operation is executed in a cloud automatically on schedule or manually at any time.

Pros:

  • Skyvia allows you to map one Dynamics 365 Business Central table to several related Xero tables. When integrating data with different structure Skyvia is able to preserve source data relations in target.
  • Visual configuration, no code required for basic scenarios
  • Scheduling and incremental sync support

Cons:

  • Still subject to both platforms' API rate limits — the tool doesn't bypass them
  • Complex dimension-to-tracking-category transformations may require custom expressions
  • Free Data Integration plan allows you to import and export up to 10,000 records per month. Large migrations require paid plans.
  • Zapier and Make lack deep financial object support — no journal entry or payment allocation handling. They're better suited for lightweight post-cutover automation (e.g., new contacts, approved invoices) than replaying accounting history.

Complexity: Medium

3. Custom API-Based ETL Pipeline

How it works: Write scripts (Python, Node.js, or C#) that extract data from Business Central's OData/REST APIs, transform it according to your mapping rules, and load it into Xero's Accounting API.

When to use it: Enterprise migrations with 10,000+ transactions, complex dimension flattening requirements, multi-entity rollups, or strict audit trail needs.

Pros:

  • Full control over transformation logic, error handling, and retry strategies
  • Can batch up to 50 invoices per Xero API request to maximize throughput
  • Can implement deduplication logic, idempotency keys, and validation checkpoints
  • Can use BC's $batch endpoint to extract efficiently

Cons:

  • Requires dedicated engineering time (typically 2–4 weeks for a production-grade pipeline)
  • Must implement OAuth 2.0 token refresh logic for both platforms
  • You own the maintenance and error resolution

Complexity: High

4. Managed Migration Service

How it works: A specialist team handles the full extract-transform-load cycle, including data audit, mapping, API scripting, validation, and cutover coordination.

When to use it: When you can't afford data loss, when you lack in-house API expertise, or when the migration is time-sensitive and your engineering team is already at capacity.

Pros:

  • Fastest time to completion (days, not weeks)
  • Vendor absorbs the risk of API failures, rate limit management, and edge cases
  • Experienced teams have pre-built handling for common BC-to-Xero gotchas

Cons:

  • Higher direct cost than DIY
  • Requires trust and clear communication with the service provider

Complexity: Low (for you)

Migration Approach Comparison

Factor Native CSV iPaaS (Skyvia) Custom ETL Managed Service
Cost Free $ $$ (eng time) $$
Volume Capacity < 500 records/batch Medium Unlimited (with batching) Unlimited
Relationship Preservation None Partial Full Full
Dimension Handling Manual flattening Expression-based Programmatic Programmatic
Error Recovery Re-upload entire file Per-record logs Custom retry logic Handled by vendor
Engineering Effort Low Low–Medium High None
Time to Complete Days (manual) Days–Weeks 2–4 weeks 3–7 days
Best For Startups, simple CoA Mid-market, ongoing sync Enterprise, custom logic Time-sensitive, complex

Which Approach Should You Choose?

  • Small business, one-time migration, low engineering bandwidth: CSV or the Conversion Toolbox, but only if you need current balances, open AR/AP, and limited history.
  • Mid-market firm needing posted invoices, bills, payments, and ongoing sync: iPaaS or API-led migration.
  • Enterprise or dimension-heavy Business Central environment: Custom ETL or a managed migration partner.
  • Tight timeline and zero tolerance for data loss: Managed service.

API rate limits are the single biggest technical bottleneck in this migration. You're bounded on both sides.

Business Central (Source)

Each user or service principal is limited to 5 concurrent requests, up to 100 queued connections, and 6,000 requests per 5-minute rolling window. These per-user limits replaced the old environment-level model starting in late 2023.

The old documentation you'll find online references 600 requests/minute for production environments — that was the previous environment-level model. The current model is per-user/per-service-principal.

BC's OData layer allows a max page size of 20,000 and up to 100 operations per $batch request, which is useful for extract efficiency.

Extraction strategies:

  • The OData $batch endpoint lets you bundle multiple create, update, or delete operations into a single HTTP request. Each batch counts as one request against your rate limit, making it much more efficient for bulk operations.
  • Use $expand to fetch parent and child records (e.g., invoice headers + lines) in a single call
  • Use $filter with lastModifiedDateTime for incremental extraction
  • Distribute load across multiple service principals if needed

For more on handling complex BC data extraction, see Migrating Dynamics 365 On-Premise to Cloud: Escaping the SSIS Bottleneck with JSONata.

Xero (Target)

Concurrent Limit: 5 API calls in progress at a time (per-organization, per-app). Minute Limit: 60 API calls per minute (per-organization, per-app). Daily Limit: 5,000 API calls per day (per-organization, per-app). App-wide Minute Limit: 10,000 calls per minute across all tenancies for a single app.

Xero access tokens expire after 30 minutes. To maintain uninterrupted access, request the offline_access scope to receive a refresh token. Refresh tokens should be renewed proactively before expiry using the /connect/token endpoint.

For organizations in Australia, New Zealand, the UK, or the US, Xero also offers Custom Connections that use a client credentials model and avoid refresh-token management. If you're outside that model, use standard OAuth and plan token refresh logic into your pipeline.

The invoice math: Syncing a single invoice to Xero can require multiple API calls — create/verify the contact, create the invoice, attach a payment, optionally add a file attachment. Even with batching, this adds up quickly.

While there is no upper limit in the number of nodes that can be sent at one time, a ceiling of about 50 nodes per request is practical — this will ensure a request does not exceed the maximum size of 3.5MB.

By batching 50 invoices per POST /Invoices call, you reduce a 5,000-invoice import from 5,000 API calls to 100 — well within the daily limit. Without batching, you'll stall after the first day.

Warning

2026 pricing change: All tiers will also have rate limits of 5,000 per day per organization (except for the Starter tier, which will have 1,000). If your Xero developer app is on the Starter tier after March 2026, you'll be limited to just 1,000 calls/day — a ceiling that makes non-batched migrations nearly impossible.

Xero also enforces high-volume thresholds on some GET requests where the platform would need to process more than 100,000 documents. From 2025, Xero allows a pagesize of up to 1,000 on endpoints that support paging (except Journals). Window your reads by date, status, or document class to stay under these thresholds.

Data Mapping: Chart of Accounts, Contacts, Invoices, and Payments

Every field that doesn't map cleanly will either cause import failures or silent data loss. This section covers the implementation core.

Chart of Accounts

Business Central Field Xero Field Notes
G/L Account No. Account Code Xero codes are alphanumeric, max 10 chars. Keep a permanent crosswalk.
Name Name Max 150 characters in Xero
Account Category Account Type Map BC categories to Xero's fixed types (see below)
Account Subcategory Tax Type Requires careful mapping to Xero's tax rate codes
Direct Posting (Yes/No) Enable Payments (checkbox) BC summary accounts → Xero header accounts
Dimensions (Default) Tracking Categories Only 2 dimensions can be preserved as tracking categories
Info

Xero has a fixed set of account types: BANK, CURRENT, CURRLIAB, DEPRECIATN, DIRECTCOSTS, EQUITY, EXPENSE, FIXED, LIABILITY, NONCURRENT, OTHERINCOME, OVERHEADS, PREPAYMENT, REVENUE, SALES, TERMLIAB, PAYGLIABILITY, SUPERANNUATIONEXPENSE, SUPERANNUATIONLIABILITY, WAGESEXPENSE. You cannot create custom account types. Map your BC account categories to these types during transformation.

Customers & Vendors → Contacts

Business Central Field Xero Field Notes
No. (Customer/Vendor) ContactNumber Optional in Xero, but recommended for cross-reference
Name Name
Address / City / Post Code / Country AddressLine1, City, PostalCode, Country Xero uses separate address objects for POBOX and STREET
Phone No. Phone (DEFAULT)
E-Mail EmailAddress Normalize and drop invalid formats before load
VAT Registration No. TaxNumber
Currency Code DefaultCurrency Must match Xero's ISO 4217 codes
Customer/Vendor IsCustomer/IsSupplier BC separates these; Xero unifies them as Contacts with boolean flags
Payment Terms Code PaymentTerms Xero supports limited types: DAYSAFTERBILLDATE, DAYSAFTERBILLMONTH, OFCURRENTMONTH, OFFOLLOWINGMONTH

Critical difference: Business Central maintains separate Customers and Vendors tables. Xero merges them into a single Contacts entity. If a business partner is both a customer and a vendor in BC, you must merge them into one Xero contact with both IsCustomer and IsSupplier set to true. If the same entity has slightly different names across the two tables (e.g., "Acme Corp" vs "ACME Corporation"), your transform layer must catch this. Xero will happily create two contacts, and you'll have split transaction history.

Xero states that there's a limit of only 10,000 contacts allowed in a single organization. However, we haven't found this to be a hard limitation. It is possible that having too many contacts well over 10,000 may cause performance issues when running reports within Xero. Archive inactive customers in BC before migration to keep the Xero contact list lean.

Sales Invoices → Invoices

Business Central Field Xero Field Notes
No. InvoiceNumber
Sell-to Customer No. Contact.ContactID Must resolve to an existing Xero contact
Posting Date Date
Due Date DueDate
Currency Code CurrencyCode
Amount Including VAT Total (calculated) Xero calculates totals from line items
Sales Invoice Lines LineItems [] Each BC line → one Xero LineItem
Line: G/L Account LineItem.AccountCode Must match Xero Chart of Accounts
Line: Amount LineItem.LineAmount
Line: VAT % LineItem.TaxType Map BC VAT posting groups to Xero tax rate names
Dimensions on Line LineItem.Tracking [] Only 2 tracking categories per line

Purchase invoices follow the same mapping pattern but target the ACCPAY (Accounts Payable) invoice type in Xero.

Payments

Business Central Field Xero Field Notes
Entry No. PaymentID (auto-generated)
Amount Amount
Posting Date Date
Applies-to Doc. No. Invoice.InvoiceID Must link to the corresponding Xero invoice
Bank Account Account.Code Must be a BANK-type account in Xero
Danger

Duplicate bank feeds risk: If you import payments AND connect Xero to the same bank account for automatic bank feeds, the same transaction will appear twice — once from your import and once from the feed. Set Xero's lock date to the day before your go-live date to prevent imported historical transactions from interfering with live bank reconciliation. This is the most common mistake in financial migrations to Xero.

For more on avoiding costly mistakes during financial data migration, see 7 Costly Mistakes to Avoid When Migrating Financial Data.

Attachments

Attachments deserve separate attention. Business Central's API defines attachments as Incoming Documents, while the client also supports record-level attachments — these are not the same surface. Do not promise attachment completeness until you test both extraction paths. On the Xero side, the Files API supports associations to contacts and accounting documents, with paging to retrieve up to 100 associations.

Tax Mapping

BC's matrix of VAT Bus. Posting Group × VAT Prod. Posting Group must be flattened to Xero's single tax rate per line item. Map business logic, not labels — do not assume a matching name implies matching tax behavior. Unmapped tax codes cause the entire batched API payload to fail, not just the affected record.

Step-by-Step Execution and Validation

Phase 1: Pre-Migration Audit

  1. Export your BC dimension usage report. Identify which dimensions are actively used on posted entries. Decide which two become Xero tracking categories.
  2. Run a data quality scan. Flag customers/vendors with missing emails, duplicate names, or inconsistent address formats.
  3. Inventory your Chart of Accounts. Archive or merge inactive accounts. Xero performs better with a lean CoA.
  4. Document your VAT/Tax posting groups. Map the BC matrix to Xero's single tax rate model before writing any code.
  5. Set your conversion date. Align it with a month-end close so your opening balances are clean. Design everything else around this date: opening balances, open documents, historical cut line, bank feed reconnect dates, and validation reports.
  6. Audit transaction volumes. Confirm your Business Central volumes fall within Xero's advisory monthly limits.

For financial systems, a big-bang cutover at month-end or fiscal boundary is usually cleaner than a phased migration. Use phased patterns only if you have explicit ownership for bank feeds, payments, and duplicate prevention during the coexistence window.

Phase 2: Extract from Business Central

Use the BC API v2.0 for programmatic extraction:

GET /api/v2.0/companies({companyId})/salesInvoices
  ?$filter=postingDate ge 2024-01-01
  &$expand=salesInvoiceLines($expand=account)
  &$top=100
Accept: application/json
Authorization: Bearer {access_token}

Page through results using @odata.nextLink. If response can't be provided within 10 minutes, the service aborts the request and returns 504 - Gateway Timeout. For large datasets, use date-range filters to keep each request under the timeout threshold.

For Configuration Package exports (XML-based bulk export), use BC's RapidStart Services to extract Customers, Vendors, Items, and G/L Entries as structured packages.

Phase 3: Transform

This is where the real work happens:

  1. Merge Customers + Vendors into a unified Contacts list. Deduplicate on name + tax number.
  2. Flatten dimensions into tracking category assignments (top 2) or account code suffixes (remaining).
  3. Map tax codes. BC's VAT Bus. Posting Group × VAT Prod. Posting Group matrix → single Xero TaxType string per line.
  4. Convert currency amounts. Ensure exchange rates match. Xero stores amounts in the invoice currency and converts using its own rate table.
  5. Structure multi-line invoices. Each BC sales invoice line becomes a LineItem in the Xero payload.

Phase 4: Load into Xero

Load order matters. Follow this sequence to avoid broken references:

  1. Chart of Accounts
  2. Tracking Categories + Options
  3. Contacts (merged Customers + Vendors)
  4. Invoices (Sales → ACCREC, Purchase → ACCPAY)
  5. Credit Notes
  6. Payments (linked to invoice IDs from step 4)
  7. Manual Journals (for opening balances and adjustments)

Batch invoices into groups of 50 per API call:

import requests
import time
 
XERO_API_URL = "https://api.xero.com/api.xro/2.0/Invoices"
 
def load_invoices_batched(invoices, headers, batch_size=50):
    """Load invoices to Xero in batches of 50."""
    for i in range(0, len(invoices), batch_size):
        batch = invoices[i:i + batch_size]
        payload = {"Invoices": batch}
        
        response = requests.post(
            XERO_API_URL,
            json=payload,
            headers=headers
        )
        
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            time.sleep(retry_after)
            # Retry the same batch
            response = requests.post(
                XERO_API_URL,
                json=payload,
                headers=headers
            )
        
        if response.status_code != 200:
            log_errors(batch, response.json())
            continue
        
        # Respect minute rate limit
        time.sleep(1)  # ~60 calls/min max
Warning

API Limit Warning: Even with batching, pushing tens of thousands of historical invoices can take days. Plan your migration window accordingly and prioritize the current fiscal year if time is short.

Phase 5: Validate

  • Record counts: Compare total customers, vendors, invoices, and payments between BC and Xero.
  • Balance verification: Run a Trial Balance in BC as of the conversion date. Compare to Xero's Trial Balance. They must match to the penny.
  • Spot-check samples: Pick 20–30 invoices at random. Verify line items, tax amounts, tracking categories, and payment allocations.
  • Aged receivables/payables: Compare aging reports. Mismatches here indicate payment-to-invoice linking failures.
  • Attachment verification: Spot-check that key documents transferred with correct associations.

Validate incrementally — year-by-year during the historical load — rather than waiting until the end to discover that contacts merged badly or tax codes drifted.

Edge Cases and Common Failures

Multi-Currency Transactions

Business Central stores transactions in both local and foreign currency with exchange rates on each entry. Xero also supports multi-currency but applies rates differently. If your BC instance uses adjusted exchange rates (e.g., for unrealized gains/losses), those adjustments need to be re-entered as manual journals in Xero. Historical exchange rates must be calculated and locked before pushing to Xero to avoid FX variance.

Partial Payments and Overpayments

BC supports complex payment application where one payment can apply to multiple invoices. Xero handles this through the Payments API and Overpayments/Prepayments objects. You'll need to reconstruct these allocations explicitly — they fail if invoices are loaded in the wrong order or target IDs are missing.

Voided and Reversed Entries

There's no way to delete invoices in Xero, we are only able to void invoices. If your BC data contains reversed entries, decide whether to import them as voided invoices or exclude them entirely.

Journal API Is Read-Only

The /Journals endpoint does not support POST or PUT. You can only read system journals. To create journal entries, use /ManualJournals instead. This catches many teams off guard. You cannot import historical system-generated journals. Use /ManualJournals for opening balances and adjustments.

Duplicate Bank Feed Entries

If you import historical payments and reconnect bank feeds too far back, Xero's bank feed will attempt to reconcile them again. Mark historical invoices as paid against a dedicated clearing account, not your active bank feed account.

Header vs. Line-Level Dimensions

A line-level dimension strategy in BC does not survive in Xero if you only map header-level context. Ensure your extraction captures dimensions at the invoice line level and maps them to LineItem.Tracking [].

Missing Tax Rates

Xero requires strict tax rate mapping. Unmapped Business Central tax codes cause the entire batched API payload to fail — not just the affected record. Validate your full tax code map before loading any transactions.

Duplicate Contacts

If the same entity exists as both a BC Customer and a BC Vendor with slightly different names, your transform layer must catch this. Xero will create two contacts, and you'll have split transaction history that's painful to fix retroactively.

Xero's Hard Constraints: What You'll Lose

Be upfront with stakeholders about what Xero cannot do:

Business Central Capability Xero Reality
Unlimited dimensions 2 tracking categories, ~100 options each
Custom AL extensions / page customizations No custom objects or fields
Multi-entity consolidation (IC transactions) Separate Xero orgs, no native consolidation
Advanced inventory (BOM, assembly, serial/lot tracking) If you come close to the 4,000 tracked inventory item limit in Xero, you probably shouldn't be using Xero for tracking inventory.
Revenue recognition schedules Not natively supported
Job costing / project accounting Xero Projects (limited)
Approval workflows (BC Power Automate) Basic approval via Xero's invoice approval
Report builder (Account Schedules / Financial Reports) Standard reports + tracking category filters only

Any custom Business Central data must be stored in the Contact's Notes field, appended to line item descriptions, or kept in an external system. Xero does not support custom data tables — there is no equivalent to BC's table extensions.

Validation, UAT, and Rollback

Validation is where financial migrations succeed or fail.

Run these checks:

  • Record counts by entity
  • Chart of Accounts crosswalk review
  • Trial balance at the conversion date
  • AR and AP aging comparison
  • Sample invoices with line totals, tax, due dates, and payment allocations
  • Sample attachment verification
  • User acceptance testing with the finance team in a Xero sandbox environment before production cutover

Your rollback plan should answer three questions before go-live:

  1. How do you stop new writes to the old system?
  2. How do you rerun only the failed layer?
  3. How do you restore Xero to a known-good state if the final load is rejected?

If the trial balance is off and cannot be easily reconciled, wipe the Xero tenant and restart the load phase. Set lock dates in Xero after validation sign-off, not before — this prevents accidental modifications to closed periods while leaving room for corrections during UAT.

Post-Migration Tasks

  1. Set the Xero lock date to the conversion date. This prevents anyone from editing imported historical transactions.
  2. Connect bank feeds. Only after confirming that no imported transactions overlap with incoming feed data.
  3. Rebuild automations. BC workflows (Power Automate, approval flows) have no equivalent export. Recreate them in Xero or connected tools.
  4. Rebuild recurring invoices and bank rules in Xero.
  5. Train your team. Xero's reconciliation workflow differs significantly from Business Central's journal entry process. Budget time for finance team onboarding.
  6. Monitor for 30 days. Compare P&L, Balance Sheet, and Aged Receivables weekly between BC (frozen) and Xero (live).
  7. Decommission BC access only after the validation period. Keep read-only access for historical reference.
  8. Route non-accounting data to the right systems. CRM-like objects (leads, opportunities, activities) and custom extension data don't belong in Xero. Keep them in dedicated CRM or operational systems.

Best Practices Checklist

  • Back up everything. Export a full BC Configuration Package and database backup before touching anything.
  • Run a test migration against a Xero demo company first. Xero provides free demo orgs for this purpose.
  • Validate incrementally. Load Chart of Accounts → validate. Load Contacts → validate. Load Invoices → validate.
  • Use idempotency keys. If your script crashes mid-batch, you need to resume without creating duplicates. Store Xero-generated IDs for every record you create.
  • Log every API response. Both success and failure. You'll need this audit trail when reconciling.
  • Set the conversion date deliberately. Align with a month-end close for clean opening balances.
  • Communicate the cutover plan. Finance teams, AP/AR clerks, and external accountants all need to know when to stop using BC and start using Xero.
  • Keep a permanent archive of source IDs, dimension detail, and legacy attachments.
  • Document every mapping rule and every deliberate data compromise.

Automation Script Outline

Here's a high-level Python structure for an API-based migration pipeline:

# bc_to_xero_migration.py — High-level structure
 
class BCExtractor:
    """Handles OAuth2 auth and paginated extraction from BC API v2.0."""
    def __init__(self, tenant_id, client_id, client_secret, company_id):
        self.base_url = f"https://api.businesscentral.dynamics.com/v2.0/{tenant_id}/production/api/v2.0"
        self.company_id = company_id
        self.token = self._authenticate(client_id, client_secret)
    
    def extract_customers(self, since=None):
        """Paginated extraction with optional lastModifiedDateTime filter."""
        # GET /companies({id})/customers?$filter=lastModifiedDateTime ge {since}
        pass
    
    def extract_invoices(self, date_from, date_to):
        """Extract with $expand for line items and dimensions."""
        # GET /companies({id})/salesInvoices?$expand=salesInvoiceLines&$filter=...
        pass
 
class Transformer:
    """Maps BC objects to Xero-compatible payloads."""
    def __init__(self, dimension_map, tax_code_map, account_map):
        self.dimension_map = dimension_map  # BC dimension -> Xero tracking category
        self.tax_code_map = tax_code_map    # BC VAT groups -> Xero TaxType
        self.account_map = account_map      # BC G/L No. -> Xero AccountCode
    
    def merge_customers_vendors(self, customers, vendors):
        """Deduplicate and merge into unified Xero Contact objects."""
        pass
    
    def transform_invoice(self, bc_invoice):
        """Convert BC invoice + lines to Xero Invoice payload."""
        pass
 
class XeroLoader:
    """Handles OAuth2 auth, batching, rate limiting, and loading to Xero."""
    def __init__(self, client_id, client_secret, tenant_id):
        self.base_url = "https://api.xero.com/api.xro/2.0"
        self.daily_calls = 0
        self.MAX_DAILY = 5000
        self.BATCH_SIZE = 50
    
    def load_contacts(self, contacts):
        """Batch load contacts, respecting 60/min limit."""
        pass
    
    def load_invoices(self, invoices):
        """Batch 50 invoices per POST, with 429 retry logic."""
        pass
    
    def load_payments(self, payments, invoice_id_map):
        """Link payments to Xero invoice IDs from load_invoices."""
        pass
 
class Validator:
    """Post-load validation: record counts, balance checks, sampling."""
    def compare_trial_balances(self, bc_tb, xero_tb):
        pass
    
    def spot_check_invoices(self, sample_size=30):
        pass
 
# Orchestration
if __name__ == "__main__":
    extractor = BCExtractor(...)
    transformer = Transformer(...)
    loader = XeroLoader(...)
    validator = Validator(...)
    
    # 1. Extract
    customers = extractor.extract_customers()
    vendors = extractor.extract_vendors()
    invoices = extractor.extract_invoices("2020-01-01", "2026-04-01")
    
    # 2. Transform
    contacts = transformer.merge_customers_vendors(customers, vendors)
    xero_invoices = [transformer.transform_invoice(inv) for inv in invoices]
    
    # 3. Load (in dependency order)
    loader.load_chart_of_accounts(transformer.map_accounts(...))
    contact_id_map = loader.load_contacts(contacts)
    invoice_id_map = loader.load_invoices(xero_invoices)
    loader.load_payments(payments, invoice_id_map)
    
    # 4. Validate
    validator.compare_trial_balances(...)
    validator.spot_check_invoices()

This is a starting point, not production-ready code. A real implementation needs error queuing, token refresh middleware, checkpoint/resume logic, dead-letter handling, and comprehensive logging. At a minimum, capture the source ID, target ID, batch number, request payload hash, response code, validation errors, retry count, and final disposition for every record.

When to Build In-House vs. Bring in a Specialist

Build in-house if:

  • Your data volume is small (< 1,000 invoices, < 500 contacts)
  • You have a developer who understands both BC's OData API and Xero's Accounting API
  • Your dimension structure is simple (≤ 2 dimensions actively used)
  • You have 3+ weeks of runway before the cutover deadline

Bring in a specialist if:

  • You have 10,000+ historical invoices or complex multi-currency setups
  • Your BC instance uses 4+ active dimensions that need intelligent compression
  • You're operating under a tight timeline (e.g., fiscal year-end cutover)
  • Your engineering team is already allocated to product work
  • You cannot tolerate any data loss or reconciliation errors on financial records

The hidden cost of DIY is not the initial build — it's the debugging. A single payment-to-invoice linking error can cascade through your aged receivables report and take days to trace. When your CFO asks why the Trial Balance doesn't tie, the cost of a specialist's fee looks very different.

At ClonePartner, we've handled ERP-to-Xero migrations with decades of transaction history. Our approach uses intelligent API batching (50 invoices per Xero request), programmatic dimension flattening, and automated lock-date management to ensure historical integrity. We extract and load the bulk of your history in the background, executing the final delta sync over a single weekend for zero-downtime cutover.

Frequently Asked Questions

How long does a Business Central to Xero migration take?
A small business with under 1,000 invoices can migrate in 2–5 days using CSV imports. Larger migrations with 10,000+ transactions typically take 1–3 weeks via API, depending on transformation complexity and Xero's 5,000 daily API call limit. A managed service can often compress this to under a week through intelligent batching.
What data do you lose migrating from Business Central to Xero?
The main losses are: dimensions beyond 2 (Xero only supports 2 tracking categories), custom AL extensions, multi-entity consolidation, advanced inventory tracking (Xero caps at 4,000 tracked items), revenue recognition schedules, and detailed approval workflows. Shortcut dimensions 3–8 must be flattened into account codes or archived externally.
What are Xero's API rate limits for migration?
Xero enforces 5 concurrent requests, 60 calls per minute, and 5,000 calls per day per organization per app. Each POST can batch up to 50 records (e.g., 50 invoices). The Starter developer tier drops to just 1,000 calls/day after March 2026.
Can I migrate Business Central dimensions to Xero?
Only partially. Xero supports exactly 2 active tracking categories with approximately 100 options each. Map your two most important BC Global Dimensions to these categories. All other dimensions must be encoded into your Chart of Accounts structure or stored in an external data warehouse for historical reporting.
Can Zapier or Make handle a full Business Central to Xero migration?
Not well. They lack deep financial object support — no journal entry or payment allocation handling. They're fine for lightweight post-cutover automation (e.g., new contacts, approved invoices), but not for replaying historical invoices, payments, credits, and reconciliation-sensitive relationships at scale.

More from our Blog

7 Costly Mistakes to Avoid When Migrating Financial Data
Accounting

7 Costly Mistakes to Avoid When Migrating Financial Data

One error can corrupt your entire history. This in-depth guide reveals the 7 costliest mistakes to avoid, including botching opening balances, incorrect data mapping, and failing to run parallel reports. We cover the "what not to do" pitfalls, from "Garbage In, Garbage Out" to ignoring multi-currency complexities. Read this before you migrate to ensure 100% data integrity, avoid tax season nightmares, and achieve a stress-free "go-live" on your new accounting system.

Raaj Raaj · · 13 min read
Migrating Dynamics 365 On-Premise to Cloud: Escaping the SSIS Bottleneck with JSONata
Microsoft Dynamics 365/From The Migration Trenches

Migrating Dynamics 365 On-Premise to Cloud: Escaping the SSIS Bottleneck with JSONata

If you are migrating Microsoft Dynamics 365 from on-premise to the cloud, standard tools like SSIS and KingswaySoft often cause project-stalling bottlenecks. This technical guide details how to replace slow, UI-bound SSIS packages with self-contained, JSONata-powered binaries. By leveraging declarative YAML mappings and automation , engineering teams can bypass workflow fatigue, execute complex data merges, and reduce debugging cycles from four hours to just twenty minutes.

Raaj Raaj · · 7 min read