Most helpdesk migrations look simple on paper. You have data here. You want it there. There are tools for that. Job done.
The Resilia project was not that.
Resilia — a multi-brand ecommerce operation — had grown organically across three distinct brands, each with its own Freshdesk instance, its own ticket structure, its own custom fields, its own agent hierarchy, and its own years of accumulated customer history. The decision had been made to consolidate everything into a single Zendesk environment. The brief: migrate all of it. Lose none of it. Keep the brands distinct within a unified workspace.
The total ticket count across three instances came to 230,000+. The largest single instance had over 150,000 tickets. And the data structures between Freshdesk and Zendesk — while superficially similar — were different enough in the details that no existing migration tool could handle the mapping correctly.
So we built one.
The problem: 3 systems with 3 different structures
The first thing I do before any migration is a full structural audit of both the source and the destination. Not just "what data exists" but "how is it organised, what's the underlying logic, and where are the mismatches."
In this case, the mismatches were significant:
Custom field schemas didn't align. Each Freshdesk instance had its own set of custom ticket fields — some shared across brands, some brand-specific, some duplicated under different names. None of them mapped cleanly to Zendesk's field structure. Every field needed to be individually reviewed, mapped, and in some cases transformed before migration.
Ticket status logic differed. Freshdesk and Zendesk use different status taxonomies. "Pending" in Freshdesk doesn't map exactly to any single Zendesk status — it depends on whether the ticket is waiting on the requester or the assignee. Automating that mapping required business logic, not just a field lookup.
Agent and group structures needed rebuilding. The agent hierarchies in three separate Freshdesk instances needed to be consolidated and rebuilt in Zendesk before tickets could be migrated — because tickets reference agents, and those agents needed to exist in Zendesk first.
Brand separation within a unified instance. Zendesk supports multi-brand configurations, but the setup needs to be deliberate. Tickets from Brand A needed to land in Brand A's queue, routed correctly, with Brand A's macros and workflows. Getting that right before migration, not after, was critical.
Why off-the-shelf migration tools fail at this scale
There are several commercial migration tools for helpdesk platforms — Help Desk Migration, Trello Data Exporter, platform-native export/import tools. They work well for straightforward single-instance migrations with standard field structures. They fall apart when the complexity increases.
The specific failure modes I've seen from commercial tools on complex projects:
- They map fields by name — which breaks immediately when source and destination field names don't match exactly
- They don't handle API rate limiting gracefully — they hit Zendesk's rate limits, fail silently, and you lose chunks of data without knowing it
- They don't support custom transformation logic — if status X in system A needs to become status Y or Z in system B depending on a condition, commercial tools can't do that
- Their error recovery is primitive — a failure mid-migration often means starting over, which at 150,000 tickets is not an option
- They don't maintain referential integrity — if a ticket references an agent, a group, or a contact that hasn't been created yet in the destination, the migration breaks or the reference gets dropped
At 230,000 tickets across three brands, a 1% data loss rate means 2,300 missing records. A 5% rate means 11,500. Those aren't acceptable numbers when those records represent real customer conversations, real support history, real evidence of what was promised and what was resolved.
Building the migration engine
The solution was a custom API-based migration application — built to handle this specific project, with the flexibility to accommodate the edge cases that always emerge mid-migration.
The core components:
Freshdesk API extractor. Pulls tickets, contacts, agents, groups, and custom field definitions from each Freshdesk instance. Stores them in a structured intermediate format — not imported directly into Zendesk, but staged for validation and transformation first.
Transformation layer. This is where the business logic lives. Field mapping rules. Status translation logic. Brand tagging. Agent reference resolution. Every record passes through this layer before it touches Zendesk. Every transformation is logged.
Validation engine. Before any batch of records goes to Zendesk, they're validated against the destination schema. Missing required fields get flagged. Agent references that don't resolve get queued for manual review. Records that would fail import get separated out with an error log — not silently dropped.
Zendesk API writer with rate limit management. Zendesk's API has per-minute and per-day rate limits. At 230,000 tickets with multiple comments and attachments each, staying within those limits over a multi-day migration required a proper rate limit manager — not just exponential backoff, but an actual queue system that tracked consumption and scheduled batches accordingly.
Resumable migration state. Every imported record gets logged with its source ID, destination ID, and import timestamp. If the migration stops for any reason — network interruption, API error, planned maintenance — it resumes from the last successful record. No re-importing, no duplication risk.
[Freshdesk API] → Extract tickets (paginated, all 3 instances)
↓
[Staging DB] → Store raw records with source metadata
↓
[Transform] → Map fields, resolve agents, apply brand tags, translate status
↓
[Validate] → Schema check, referential integrity, error flagging
↓
[Rate Limiter] → Batch scheduler respecting Zendesk API limits
↓
[Zendesk API] → Import tickets, comments, attachments
↓
[Audit Log] → Source ID → Destination ID mapping, timestamp, status
The hardest part nobody talks about
Technical architecture aside, the hardest part of any large migration isn't the code. It's the sequence — the order in which things happen.
You can't import tickets before agents exist. You can't configure routing rules before groups exist. You can't test workflows before tickets exist. Every dependency has to be resolved in the right order, and the right order isn't always obvious until you're three steps in and something breaks.
The phase sequence we used:
The result — and what actually changed
The migration completed across four weeks. 230,000+ tickets, three brands, full comment histories, attachments, custom field values, agent assignments, and status histories — all moved to Zendesk with zero data loss.
But the more interesting outcome was what the consolidation enabled that wasn't possible before.
With three separate Freshdesk instances, there was no unified view of customer history across brands. A customer who had interacted with Brand A and Brand B had two separate records in two separate systems — agents couldn't see the full relationship, and there was no way to connect the dots.
In Zendesk, with a unified multi-brand environment, that customer has one record. Every interaction across every brand is visible in one timeline. Agents can see the full relationship. Routing logic can account for customer history across brands. Cross-brand reporting became possible for the first time.
The operational efficiency gains weren't incidental — they were the point. The migration wasn't just moving data. It was enabling a fundamentally better way to serve customers who engaged with multiple brands.
What this taught me about complex migrations
I've done enough of these now to have a framework. A few things I'd tell anyone planning a non-trivial helpdesk migration:
Audit before you architect. Don't design your migration approach until you understand both the source structure and the destination schema in detail. The complexity is always in the details you discover during the audit, not the ones you assumed going in.
Never migrate directly to production. Test environment, test migration, stakeholder review, then production. Every time. At any scale. The 1,000-ticket test run caught four field mapping issues and two status translation errors that would have affected tens of thousands of records if we'd gone straight to production.
Build in resumability from the start. Migrations fail. Networks drop. APIs hit limits. If your migration can't resume from the last successful record, you're either re-running everything or accepting data gaps. Both are unacceptable at scale.
Log everything, validate everything, sign off on everything. Every batch has a source count and a destination count. Every discrepancy gets investigated. The client signs off on each phase before the next begins. This isn't bureaucracy — it's how you arrive at "zero data loss" with confidence rather than hope.
The tools that handle 95% of migrations exist and work well. It's the 5% — the multi-brand consolidations, the non-standard field structures, the migrations that need custom business logic — where you need an engineer who's done it before and can build what the situation actually requires.
That's the work I find most interesting. And it's the work that requires the most care.
Need a migration that can't afford to lose data?
Whether it's 10,000 tickets or 230,000, I approach every migration the same way: full audit first, staged testing, zero assumptions, documented end to end. Fixed fee, clear timeline, no surprises.
