Desk365 to Dixa Migration: A Technical Guide
Technical guide for migrating from Desk365 to Dixa — data model mapping, API limits, transformation logic, and step-by-step process for zero-downtime cutover.
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,500+ migrations completed
- Zero downtime guaranteed
- Transparent, fixed pricing
- Project success responsibility
- Post-migration support included
Desk365 to Dixa Migration: A Technical Guide
TL;DR — Desk365 to Dixa Migration
Migrating from Desk365 to Dixa moves your support operation from a ticket-centric, Microsoft 365-native helpdesk to a conversation-centric, push-based omnichannel routing platform. The two systems use fundamentally different data models: Desk365 stores discrete tickets with statuses and categories; Dixa models everything as conversations routed through Flows and Queues. A typical migration for 50,000–150,000 tickets takes 2–4 weeks including data mapping, test migrations, and cutover. The biggest technical challenges: Desk365's ticket replies must map into Dixa conversation messages, Dixa's import endpoint only supports email and genericapimessaging channel types, and Desk365 automations and SLAs cannot be migrated — they must be rebuilt as Dixa Flows manually. The Desk365 API v3 caps extraction at 10,000 tickets per hour; Dixa's API allows 10 requests per second per token with a daily ceiling of 864,000 requests. Dixa does not deduplicate on re-import — there is no native idempotency — so every retry without deduplication logic creates duplicate conversations. Teams with fewer than 10,000 tickets and no complex custom fields can attempt a scripted DIY migration (60–100 engineer-hours). For anything larger or with multi-level custom fields, a managed migration service is the safer path.
For post-migration testing, see Post-Migration QA: 20 Tests to Run After Your Help Desk Data Migration. For Dixa-side extraction details, see How to Export Data from Dixa: Methods, API Limits & Data Mapping. For Desk365 source-side extraction, see How to Export Data from Desk365: Methods, API Limits & Portability.
Overview: Why Teams Move from Desk365 to Dixa
Desk365 is a cloud-based, AI-enhanced helpdesk built for the Microsoft 365 ecosystem. It enables customer service through Microsoft Teams, Email, Web Forms/Widgets, and a support portal (per the Desk365 product documentation). It stores data as discrete tickets with statuses, priorities, types, groups, and custom fields — a traditional ITSM-style model.
Dixa is a conversation-centric omnichannel platform that routes phone, email, chat, and social conversations through a single intelligent routing engine. All Dixa plans include every channel natively — phone, email, live chat, WhatsApp, Instagram, Facebook Messenger, and SMS (per Dixa's pricing documentation). Instead of ticket queues and views, Dixa uses Flows (visual routing logic that evaluates conditions like channel, language, tags, and time-of-day to direct conversations) and Queues (ordered agent pools that receive conversations pushed by Flows) to route conversations to agents in real time.
Teams typically migrate from Desk365 to Dixa for three reasons:
- Omnichannel consolidation. Desk365 channels are largely limited to email, Teams, and web forms. Dixa natively routes voice, email, chat, WhatsApp, and social messaging through a single engine — no add-ons.
- Push-based routing architecture. Desk365 uses traditional pull-based views and round-robin assignment. Dixa's offer-based routing pushes conversations to agents based on skills, capacity, and priority — reducing cherry-picking and idle time.
- Scaling beyond Microsoft 365. Teams whose customer base extends beyond the M365 ecosystem often outgrow Desk365's Microsoft-centric design.
This migration is an operating model change, not just data portability. You cannot simply rename columns and import — you must translate the schema and rebuild your routing logic.
Core Data Model Differences
| Concept | Desk365 | Dixa |
|---|---|---|
| Core unit | Ticket | Conversation |
| Customer record | Contact + Company | End User |
| Agent grouping | Groups | Teams + Queues |
| Routing model | Round-robin / manual assignment | Flow-based, push routing |
| Custom fields | Custom ticket fields (cf_ prefix) |
Custom Attributes (UUID-based) |
| Automation | Automation rules, SLA policies | Flows (visual builder) |
| Categories | Type field (Question, Incident, Problem, Request) + custom types | Tags + Custom Attributes |
| Knowledge base | Built-in KB | Built-in KB |
| Ticket status | Open, Pending, Resolved, Closed + custom statuses | Open, Closed (binary) |
| Company object | Native Company entity linked to Contacts | No native company entity |
The status model is a key structural difference. By default, Desk365 comes with predefined ticket statuses — Open, Pending, Resolved, and Closed — plus the ability to create custom statuses (per Desk365 documentation). Dixa simplifies this to Open/Closed, with conversation state managed by routing queues rather than status fields.
Migration Approaches
1. Native CSV Export/Import
How it works: Desk365's export feature creates a copy of your ticket data as a CSV file, accessible in a few steps from the ticket list view (per Desk365 export documentation). You export Desk365 tickets as CSV, transform the data in a spreadsheet or script, then attempt to load into Dixa.
When to use: Small datasets under 5,000 tickets where conversation history is not essential.
Pros:
- No coding required for extraction
- Fast initial data pull
- Reasonable for contact/company seeding
Cons:
- Desk365 CSV exports do not include full conversation threads (replies, notes) — only ticket metadata
- Dixa has no native CSV import for conversations; you still need the API for loading
- Attachments are not included in CSV exports
- Manual transformation is error-prone at scale
Complexity: Low (extraction) / Medium (loading into Dixa still requires API)
2. API-Based Migration (Desk365 API → Dixa API)
How it works: Extract tickets, contacts, and companies from Desk365 using the v3 REST API. Transform the data to match Dixa's conversation model. Load into Dixa via POST /v1/conversations/import for historical conversations and the End Users API for contacts.
According to the Desk365 API v3 documentation, Version 3 allows you to retrieve 30, 50, or 100 tickets per call using the ticket_count parameter, with a maximum of 10,000 tickets per hour.
When to use: Any migration where you need full conversation history, custom fields, and relationships preserved.
Pros:
- Full control over data extraction and transformation
- Preserves conversation threading and timestamps
- Handles custom fields, tags, and attachments
- Scriptable and repeatable
Cons:
- Requires 80–150 engineer-hours for 50K+ tickets
- The conversation channel for Dixa's import endpoint currently supports values of
emailandgenericapimessagingonly (per Dixa API documentation). Phone and chat history from Desk365 must be normalized into one of these channel types. - Rate limit management on both sides
- No idempotency on Dixa's import endpoint — re-running the same payload creates duplicate conversations
Complexity: High
3. Third-Party Migration Tools
As of January 2025, no established third-party tool (Help Desk Migration, Import2, etc.) offers a direct Desk365-to-Dixa migration path. Desk365 is a niche platform within the M365 ecosystem, and Dixa is a specialized omnichannel platform — neither is well-supported by generic migration tools. Verify this claim against current tool listings before committing to an approach, as the ecosystem evolves.
When to use: Not viable for this specific pairing at the time of writing.
4. Custom ETL Pipeline
How it works: Build a dedicated Extract-Transform-Load pipeline using Python, Node.js, or a data integration framework (Airbyte, dbt, custom scripts). Extract from Desk365 API, stage in an intermediate database (PostgreSQL, SQLite), transform the data model, and load via Dixa API.
When to use: Large migrations (100K+ tickets), ongoing sync requirements, or enterprise environments where helpdesk data must be joined with product usage or CRM data before entering Dixa.
Pros:
- Full auditability with intermediate staging
- Handles complex transformations (multi-level custom fields, status mapping)
- Rerunnable with idempotent writes (if you implement deduplication in your staging layer)
- Can be extended for ongoing sync
- Creates a permanent data backup
Cons:
- Highest development effort (120–200 engineer-hours)
- Requires infrastructure for staging database
- Overkill for small, one-time migrations
Complexity: High
5. Middleware Platforms (Zapier, Make, Power Automate)
How it works: Use a middleware platform to connect Desk365 triggers to Dixa actions. Desk365 provides APIs and webhooks for integration with internal systems, and connectors for tools such as Jira and Azure DevOps (per Desk365 integration documentation). Desk365 also has a Power Automate connector. Dixa has limited native integrations on these platforms.
When to use: Ongoing sync of new tickets during a parallel-run period, not for historical data loads.
Pros:
- No-code/low-code setup
- Good for real-time forwarding during transition
Cons:
- Cannot handle bulk historical migration
- Dixa's API limitations make conversation import via middleware impractical
- Rate limits and execution timeouts
- No error recovery or retry logic for bulk operations
Complexity: Low
6. Managed Migration Services
How it works: Partner with a specialized engineering team to handle extraction, mapping, testing, and the final cutover.
When to use: High data volumes, complex relational mapping, tight cutover windows, or when internal engineering resources are fully allocated to product development.
Pros:
- Tested accuracy and minimal downtime
- Offloads operational risk and edge-case debugging
- Completed in days, not weeks
Cons:
- Requires upfront budget
Complexity: Low (for the internal team)
Migration Approach Comparison
| Approach | Historical Data | Full Threads | Custom Fields | Attachments | Complexity | Best For |
|---|---|---|---|---|---|---|
| CSV Export + API Load | Partial | No | Limited | No | Medium | < 5K tickets, metadata only |
| API-to-API | Yes | Yes | Yes | Yes | High | 5K–100K tickets |
| Custom ETL Pipeline | Yes | Yes | Yes | Yes | High | 100K+ tickets, ongoing sync |
| Third-Party Tools | N/A | N/A | N/A | N/A | N/A | Not available for this pairing (as of Jan 2025) |
| Middleware (Zapier/Make) | No | Partial | No | No | Low | Real-time sync during transition |
| Managed Service | Yes | Yes | Yes | Yes | Low | >50K tickets, complex schema |
Recommendations by Scenario
- Small business (< 10K tickets, no custom fields): API-to-API with a Python script. Budget 60–100 engineer-hours.
- Mid-market (10K–100K tickets, custom fields): API-to-API or managed migration service. Budget 100–150 engineer-hours for DIY.
- Enterprise (100K+ tickets, complex data model): Custom ETL pipeline or managed migration service. Budget 150–250 engineer-hours for DIY.
- One-time migration with tight deadline: Managed service. Do not burn engineering weeks on scripts your team will never use again.
- Ongoing sync: Custom ETL with scheduled runs, or middleware for new-ticket forwarding during transition.
Why a Managed Migration Service May Be Warranted
When Not to Build In-House
DIY migration makes sense when your team has available engineering bandwidth, the data model is simple, and volume is under 10K tickets. Beyond that threshold, the risks compound:
- Data loss from structural mismatch. Desk365's multi-status ticket lifecycle (Open → Pending → Resolved → Closed) must be flattened into Dixa's binary Open/Closed model. Custom statuses are lost without explicit mapping to tags or custom attributes.
- Broken relationships. Contact-to-company associations in Desk365 must be rebuilt in Dixa's End User model, which does not have a native company object.
- No idempotency on import. Dixa's
POST /v1/conversations/importendpoint does not check for duplicate imports. If your script fails mid-run and you re-execute without deduplication logic, you create duplicate conversations. Every retry strategy must account for this — either by tracking imported IDs in your staging layer or by implementing a check-before-import pattern. - Hidden engineering cost. The migration script is roughly 30% of the effort. Error handling, retry logic, rate limit management, validation, and edge case resolution consume the remaining 70%. Undocumented API quirks, malformed HTML in legacy tickets, and broken attachment links all surface during production runs.
- Opportunity cost. Every engineer-hour spent on migration is an engineer-hour not spent on product.
For more on our migration process, see How We Run Migrations at ClonePartner.
Pre-Migration Planning
Data Audit Checklist
Before extracting anything, inventory what exists in Desk365:
| Data Type | What to Audit | Action |
|---|---|---|
| Tickets | Total count by status, date range, channel | Define scope (all vs. recent) |
| Contacts | Total count, duplicates, missing emails | Deduplicate before migration |
| Companies | Count, contact associations | Map to Dixa end-user custom attributes |
| Custom Fields | Field types, picklist values, usage rates | Map to Dixa custom attributes; discard unused fields |
| Automations | Rules, SLA policies, canned responses | Document for manual rebuild in Dixa |
| Knowledge Base | Article count, categories, embedded media | Plan separate KB migration (see KB Migration below) |
| Attachments | Total size, file types | Verify Dixa supports all file types; check URL expiry |
| Groups | Agent group structure | Map to Dixa Teams and Queues |
Define Migration Scope
Not all data needs to move. Common exclusions:
- Spam and test tickets. Filter by status or tag before extraction.
- Tickets older than 2–3 years. Weigh the cost of migrating rarely-accessed history vs. archiving to CSV.
- Unused custom fields. Desk365 lets you create any number of additional ticket fields (per Desk365 custom fields documentation) — audit which ones actually contain data.
- Resolved tickets with no replies. If a ticket was auto-closed with no customer interaction, it may not need to exist in Dixa.
Migration Strategy
| Strategy | Description | Risk Level | Best For |
|---|---|---|---|
| Big bang | All data migrated in one cutover window | Medium | < 50K tickets, weekend cutover |
| Phased | Migrate by date range or department | Low | 50K–200K tickets |
| Incremental | Migrate historical data first, then delta sync | Lowest | 200K+ tickets, zero-downtime requirement |
We recommend phased migration with a delta sync for most Desk365-to-Dixa projects. Extract and load all historical data up to a specific freeze date. Have agents log into Dixa to verify the data. Once validated, run a final delta sync to capture remaining tickets, update DNS records, and go live.
Data Model and Object Mapping
This is the most consequential part of the migration. Getting the mapping wrong means data loss or corrupted relationships.
Object-Level Mapping
| Desk365 Object | Dixa Equivalent | Notes |
|---|---|---|
| Ticket | Conversation | 1:1, but structure changes fundamentally |
| Ticket Reply | Message (Inbound/Outbound) | Must specify direction and author |
| Private Note | Internal Note | Via POST /v1/conversations/{id}/notes after conversation import |
| Contact | End User | Email is the primary identifier |
| Company | End User custom attribute | Dixa has no native company object |
| Group | Team + Queue | Groups map to Teams; routing logic maps to Queues |
| Agent | Agent | Create agents in Dixa first; match by email |
| Tag | Tag | Direct mapping via Tag API |
| Custom Field | Custom Attribute | Must create attributes in Dixa first; each gets a UUID |
| Automation Rule | Flow | Manual rebuild required — no migration path |
| SLA Policy | SLA (in Dixa) | Manual configuration required |
| Canned Response | Quick Reply | Manual recreation required |
| Knowledge Base Article | Knowledge Base Article | Separate migration path (see below) |
Field-Level Mapping
Per the Desk365 API v3 documentation, custom fields have been standardized with a cf_ prefix. When you retrieve tickets, custom fields appear as cf_department, cf_employee_id, etc. When sending data, you also prefix custom field names with cf_.
Dixa custom attributes must be created before import. Each attribute receives a UUID that you reference when patching conversation custom attributes via PATCH /v1/conversations/{id}/custom-attributes. Store original Desk365 ticket IDs in a dedicated custom attribute (e.g., desk365_ticket_id) for traceability and rollback.
Note on externalId: Dixa's conversation import payload does not include a native externalId field for source system references. You must create a custom attribute explicitly for this purpose. This is critical for deduplication logic and post-migration auditing.
Handling Companies
Desk365 has a native Company object linked to Contacts. Dixa does not have a dedicated company entity. Two approaches:
- Custom attribute on end users. Create a
company_namecustom attribute on Dixa end users and populate it during contact import. - Email domain grouping. Dixa can group end users by email domain for reporting, but this is not a formal company object.
Neither approach preserves the hierarchical Company → Contact relationship from Desk365. Accept this as a structural limitation. If company data drives workflows, keep the master company record in a CRM and surface it in Dixa through custom cards or synced attributes.
Status Mapping
Desk365's granular statuses must collapse into Dixa's binary model:
| Desk365 Status | Dixa State | Recommended Approach |
|---|---|---|
| Open | Open (if importing as active) | Import with state: open — only for currently active tickets |
| Pending | Closed | Add pending tag for context |
| Resolved | Closed | Standard mapping |
| Closed | Closed | Standard mapping |
| Custom statuses | Closed | Preserve original status as a tag or custom attribute |
Critical: Importing tickets as open will route them to agents via Dixa's Flow engine. Only import currently active tickets as open. All historical tickets should import as closed. Importing 50,000 historical tickets as open will flood your agents with rerouted conversations.
Multi-Level Custom Fields
Desk365 offers single-level fields (Dropdown, Text Input, Checkbox, Date, Number) and multi-level fields that dynamically populate a second or more levels when you select an element in the top level (per Desk365 custom fields documentation).
Dixa custom attributes are flat key-value pairs. Multi-level Desk365 fields must be flattened. Options:
- Concatenate levels:
"Level1 > Level2 > Level3"as a single text attribute - Create separate custom attributes for each level (e.g.,
product_category,product_subcategory) - Discard lower levels if they carry low signal — verify usage rates in the audit
Migration Architecture
Data Flow: Extract → Transform → Load
Desk365 API v3 Staging Layer Dixa API v1
┌─────────────┐ ┌──────────────────┐ ┌───────────────────┐
│ GET /v3/ │ │ PostgreSQL / │ │ POST /v1/endusers │
│ tickets │───>│ JSON files │───>│ POST /v1/ │
│ GET /v3/ │ │ │ │ conversations/ │
│ contacts │ │ Transform: │ │ import │
│ GET /v3/ │ │ - Map fields │ │ POST /v1/ │
│ companies │ │ - Thread replies │ │ conversations/ │
│ │ │ - Map statuses │ │ {id}/notes │
│ │ │ - Resolve agents │ │ PATCH /v1/ │
│ │ │ - Download │ │ conversations/ │
│ │ │ attachments │ │ {id}/custom- │
│ │ │ - Track imported │ │ attributes │
│ │ │ IDs (dedup) │ │ │
└─────────────┘ └──────────────────┘ └───────────────────┘
Desk365 API Extraction
- Base URL:
https://{yoursubdomain}.desk365.io/apis/v3/ - Authentication: API key found in the Desk365 portal: Settings > Integrations > API (per Desk365 API documentation).
- Rate limit: Maximum 10,000 tickets per hour (per Desk365 API v3 documentation).
- Pagination: Use
ticket_countparameter (30, 50, or 100 per call) with page offsets - Custom fields: Returned with
cf_prefix in v3 responses - Filtering: The default result returns all tickets sorted by creation date. You can filter by status, priority, type, group, agent, date range, and other attributes (per Desk365 API documentation).
Common Desk365 API error codes:
| HTTP Status | Meaning | Action |
|---|---|---|
| 401 | Invalid or expired API key | Regenerate key in Settings > Integrations > API |
| 403 | Plan does not include API access | Verify Desk365 plan tier |
| 404 | Invalid endpoint or ticket ID | Check URL construction |
| 429 | Rate limit exceeded (10K/hour cap) | Wait until the next hourly window |
| 500/503 | Server error | Retry with exponential backoff |
Batch your extraction requests by date range (e.g., one month at a time) to avoid hitting the 10K/hour cap during heavy pulls.
Dixa API Loading
- Base URL:
https://dev.dixa.io/v1/ - Authentication: Bearer token-based authentication (per Dixa API documentation). Tokens are created in the Dixa admin panel under Settings > Integrations > API Tokens.
- Rate limit: 10 requests per second with a short burst allowance of 4 requests, and a daily ceiling of 864,000 requests per token (per Dixa API documentation).
- Import endpoint:
POST /v1/conversations/import - Supported import channels:
emailandgenericapimessagingonly - Error handling: Exceeding the rate limit returns HTTP 429 Too Many Requests. Dixa does not send a
Retry-Afterheader — implement exponential backoff on the client side (per Dixa API documentation). - Token strategy: The limit is per token rather than per account. Splitting work across separate tokens raises total throughput (per Dixa API documentation).
- No bulk import endpoint. Each conversation requires its own API call.
- No idempotency. Re-submitting the same payload creates a duplicate conversation. Your migration script must track which records have been successfully imported.
Retrieving the emailIntegrationId
Dixa's conversation import payload requires an emailIntegrationId for email-type imports. This is the UUID of the email integration configured in your Dixa account. To retrieve it:
- Navigate to Settings > Integrations > Email in the Dixa admin panel
- Select your email integration (e.g.,
support@yourdomain.com) - The integration ID appears in the URL:
https://app.dixa.io/settings/integrations/email/{integrationId} - Alternatively, call
GET /v1/integrations/emailvia the API to list all email integrations with their IDs
Using an incorrect emailIntegrationId will cause the import to fail with a 400 error. If you're importing non-email Desk365 tickets (Teams, web form), use the genericapimessaging channel type instead, which does not require an integration ID.
Step-by-Step Migration Process
Step 1: Extract Data from Desk365
Extract tickets, contacts, and companies via the Desk365 API. Store raw responses as JSON files or in a staging database. Extract contacts first — you need them to build the ID mapping before importing conversations.
import requests
import time
import json
DESK365_BASE = "https://{subdomain}.desk365.io/apis/v3"
DESK365_API_KEY = "your-api-key"
def extract_tickets(page=1, per_page=100):
"""Extract tickets from Desk365 with pagination."""
all_tickets = []
while True:
resp = requests.get(
f"{DESK365_BASE}/tickets",
headers={"Authorization": f"Bearer {DESK365_API_KEY}"},
params={
"ticket_count": per_page,
"page": page,
"description": "html"
}
)
if resp.status_code == 429:
print("Rate limit hit. Waiting 360 seconds...")
time.sleep(360) # wait for hourly window to reset
continue
if resp.status_code == 401:
raise Exception("Invalid API key. Regenerate in Settings > Integrations > API.")
resp.raise_for_status()
data = resp.json()
tickets = data.get("tickets", [])
if not tickets:
break
all_tickets.extend(tickets)
page += 1
time.sleep(0.5) # respect rate limits
return all_tickets
def extract_contacts():
"""Extract contacts from Desk365."""
resp = requests.get(
f"{DESK365_BASE}/contacts",
headers={"Authorization": f"Bearer {DESK365_API_KEY}"}
)
resp.raise_for_status()
return resp.json().get("contacts", [])Step 2: Transform Data
The transformation layer handles the ticket-to-conversation structural change. Each Desk365 ticket becomes a Dixa conversation with messages. Internal notes must be separated for a follow-up API call.
def transform_ticket_to_conversation(ticket, agent_map, contact_map, email_integration_id):
"""Transform a Desk365 ticket into a Dixa conversation import payload.
Args:
ticket: Desk365 ticket dict from API
agent_map: dict mapping agent email -> Dixa agent UUID
contact_map: dict mapping contact email -> Dixa end user UUID
email_integration_id: UUID from Dixa Settings > Integrations > Email
"""
requester_email = ticket.get("contact_email", "")
requester_id = contact_map.get(requester_email)
if not requester_id:
return None # skip tickets with no valid requester; log for review
messages = []
# Initial ticket description as first inbound message
messages.append({
"content": {
"value": ticket.get("description", ""),
"_type": "Html"
},
"attachments": [],
"createdAt": ticket.get("created_time"),
"_type": "Inbound"
})
# Add replies as subsequent messages (exclude private notes)
for reply in ticket.get("replies", []):
if reply.get("is_private_note"):
continue # handle notes separately in Step 5
direction = "Outbound" if reply.get("is_agent_reply") else "Inbound"
msg = {
"content": {
"value": reply.get("body", ""),
"_type": "Html"
},
"attachments": [],
"createdAt": reply.get("created_time"),
"_type": direction
}
if direction == "Outbound":
agent_email = reply.get("agent_email")
msg["agentId"] = agent_map.get(agent_email)
messages.append(msg)
# Determine state: only currently active tickets should be open
desk365_status = ticket.get("status", "").lower()
state = "open" if desk365_status == "open" else "closed"
return {
"requesterId": requester_id,
"subject": ticket.get("subject", "No subject"),
"messages": messages,
"language": "en",
"_type": "Email",
"emailIntegrationId": email_integration_id,
"state": state
}Step 3: Create End Users in Dixa
Before importing conversations, all contacts must exist as Dixa end users. You cannot assign a conversation to a requester that does not exist.
Per Dixa's API documentation, GET /v1/endusers returns an empty data array (not a 404) when no match is found. Create the end-user before importing the conversation.
DIXA_BASE = "https://dev.dixa.io/v1"
DIXA_TOKEN = "your-dixa-api-token"
def create_end_user(contact, max_retries=5):
"""Create an end user in Dixa from a Desk365 contact.
Returns the Dixa end user UUID."""
payload = {
"email": contact.get("email"),
"displayName": contact.get("name", ""),
"phoneNumber": contact.get("phone", None)
}
for attempt in range(max_retries):
resp = requests.post(
f"{DIXA_BASE}/endusers",
headers={
"Authorization": f"Bearer {DIXA_TOKEN}",
"Content-Type": "application/json"
},
json=payload
)
if resp.status_code == 429:
wait = 2 ** attempt # exponential backoff: 1, 2, 4, 8, 16s
time.sleep(wait)
continue
if resp.status_code == 409:
# End user already exists — look up by email
return lookup_end_user_by_email(contact.get("email"))
resp.raise_for_status()
return resp.json().get("data", {}).get("id")
raise Exception(f"Failed to create end user after {max_retries} retries: {contact.get('email')}")Step 4: Import Conversations into Dixa
Submit the transformed payloads to Dixa's import endpoint. Log every source ID, target ID, and response — including any partialErrors Dixa returns. Quarantine records with broken attachment URLs instead of blocking the entire batch.
Critical: Implement deduplication. Dixa does not deduplicate imports. If a conversation is submitted twice, two separate conversations are created. Track successfully imported Desk365 ticket IDs in your staging database before re-running any batch.
import sqlite3
def import_conversation(payload, desk365_ticket_id, db_conn):
"""Import a conversation into Dixa with deduplication tracking.
Args:
payload: Transformed conversation payload
desk365_ticket_id: Original Desk365 ticket number for tracking
db_conn: SQLite/PostgreSQL connection for dedup tracking
"""
# Check if already imported (idempotency via staging layer)
cursor = db_conn.execute(
"SELECT dixa_id FROM migration_log WHERE desk365_id = ?",
(desk365_ticket_id,)
)
existing = cursor.fetchone()
if existing:
return {"id": existing[0], "status": "already_imported"}
resp = requests.post(
f"{DIXA_BASE}/conversations/import",
headers={
"Authorization": f"Bearer {DIXA_TOKEN}",
"Content-Type": "application/json"
},
json=payload
)
if resp.status_code == 429:
time.sleep(2) # use exponential backoff in production
return import_conversation(payload, desk365_ticket_id, db_conn)
if resp.status_code == 400:
error_detail = resp.json()
db_conn.execute(
"INSERT INTO migration_errors (desk365_id, error) VALUES (?, ?)",
(desk365_ticket_id, json.dumps(error_detail))
)
db_conn.commit()
return None
resp.raise_for_status()
result = resp.json()
dixa_id = result.get("data", {}).get("id")
# Log successful import for deduplication
db_conn.execute(
"INSERT INTO migration_log (desk365_id, dixa_id) VALUES (?, ?)",
(desk365_ticket_id, dixa_id)
)
db_conn.commit()
return resultStep 5: Apply Tags, Custom Attributes, and Internal Notes
After conversations are imported, apply tags and custom attributes in separate API calls. Private notes must be imported via POST /v1/conversations/{id}/notes — Dixa's export messages endpoint does not contain internal notes (per Dixa API documentation); they are stored separately.
def post_import_enrichment(conv_id, ticket, dixa_client):
"""Apply tags, custom attributes, and notes after import."""
# Apply tags (including mapped status and type)
tags = list(ticket.get("tags", []))
# Preserve original status as tag
original_status = ticket.get("status", "")
if original_status and original_status.lower() not in ["open", "closed"]:
tags.append(f"desk365_status:{original_status}")
# Preserve original type as tag
original_type = ticket.get("type", "")
if original_type:
tags.append(f"desk365_type:{original_type}")
for tag in tags:
dixa_client.tag_conversation(conv_id, tag)
# Patch custom attributes
custom_attrs = {}
for key, value in ticket.items():
if key.startswith("cf_") and value:
custom_attrs[key] = str(value)
# Add original ticket number for cross-reference
custom_attrs["desk365_ticket_id"] = str(ticket.get("ticket_number", ""))
if custom_attrs:
dixa_client.patch_custom_attributes(conv_id, custom_attrs)
# Import private notes
for note in ticket.get("private_notes", []):
dixa_client.add_internal_note(conv_id, note)Step 6: Validate Data
Compare record counts, spot-check conversations, and verify field mappings. See the Validation & Testing section below.
Rate limit math: At 10 requests/second, importing 50,000 conversations (plus tags, notes, and custom attributes) generates roughly 150,000–250,000 API calls. At maximum throughput, that's 4–7 hours of continuous loading — but real-world throughput with retries, validation, and 429 backoffs is typically 30–50% of theoretical maximum. Plan for 1–3 days of load time for 50K tickets. For a 200K-ticket migration with an average of 5 messages per ticket, plan for 3–10 days of load time if making separate API calls per conversation, message, note, and tag.
Edge Cases and Challenges
Duplicate Contacts
Desk365 may contain duplicate contacts (same email, different records). Dixa uses email as the primary identifier for end users. Deduplicate before migration — merge contact records in Desk365 or handle deduplication in the transform layer by keying on email address and merging metadata from duplicate records.
Internal Notes vs. Public Replies
Desk365 distinguishes between internal notes and public replies. When mapping to Dixa, ensure internal notes are strictly flagged as internal messages and imported via the notes endpoint — not included in the conversation message stream. Leaking internal comms to customers is a migration failure that is hard to undo. Validate by spot-checking 50 conversations that had private notes in Desk365 and confirming they appear only as internal notes in Dixa.
Missing or Deleted Contacts
If a Desk365 ticket is associated with a deleted contact, Dixa's API will reject the conversation import with a 400 error if the requester ID is invalid. Create a fallback "Deleted User" profile in Dixa (e.g., deleted-user@migration.internal) to assign orphaned conversations, or skip those tickets and log them for review.
Inline Images
Images pasted directly into the body of a Desk365 ticket (base64 encoded) often break during API extraction. Your transform script must:
- Parse the HTML body for
<img>tags withsrc="data:image/..."attributes - Extract the base64 string
- Decode and upload to hosted storage (e.g., S3, Azure Blob Storage)
- Rewrite the
<img>tagsrcattribute to point to the hosted URL
Without this step, inline images appear as broken references in Dixa.
Attachments
Dixa accepts attachment URLs in the import payload — the platform downloads and stores copies (per Dixa API documentation). Ensure Desk365 attachment URLs remain accessible during the migration window. If they expire or require authentication, download them first to temporary storage (S3, Azure Blob), then pass those public URLs in the Dixa API payload.
Channel Type Normalization
Desk365 tickets may originate from Teams, email, web forms, or widgets. Dixa's import endpoint supports only email and genericapimessaging (per Dixa API documentation). Mapping:
| Desk365 Channel | Dixa Import Type | Notes |
|---|---|---|
email |
Direct mapping; requires emailIntegrationId |
|
| Microsoft Teams | email |
Loses original channel context |
| Web Form | genericapimessaging |
No integration ID required |
| Web Widget | genericapimessaging |
No integration ID required |
There is no way to preserve the original channel metadata for non-email channels. Consider adding a desk365_channel tag or custom attribute to preserve this information.
API Failures and Throttling
When Dixa's rate limit is exceeded, it returns HTTP 429. Dixa does not send a Retry-After header (per Dixa API documentation). Build retry logic with:
- Exponential backoff starting at 1 second, doubling to a maximum of 32 seconds
- Jitter (random delay added to backoff) to avoid thundering herd on parallel workers
- Dead-letter queue for records that fail after 5 retries
- 429 handling: Back off and retry
- 5xx handling: Retry with jitter (server-side transient error)
- 400 handling: Log error detail and quarantine — do not retry (payload is malformed)
Webhook Handling During Parallel-Run Period
If you run Desk365 and Dixa in parallel during migration, configure Desk365 webhooks to forward new ticket events to your migration pipeline so new tickets created during the transition window are captured in the delta sync. Alternatively, set a clear freeze timestamp and run the delta extraction against Desk365's updated_after filter after the parallel period ends.
Limitations and Constraints
What Dixa Cannot Replicate from Desk365
| Desk365 Feature | Dixa Limitation | Workaround |
|---|---|---|
| Custom ticket statuses | Binary Open/Closed only | Preserve originals as tags or custom attributes |
| Company object | No native company entity | Use custom attributes on end users |
| Multi-level custom fields | Flat custom attributes only | Concatenate or split into separate attributes |
| Automation rules | Cannot be imported | Rebuild manually as Flows |
| SLA policies | Cannot be imported | Reconfigure in Dixa SLA settings |
| Canned responses | Cannot be imported | Recreate as Quick Replies |
| Microsoft Teams channel | No native Teams channel | Import as email type |
| Power Automate connector | No direct equivalent | Use Dixa API or Zapier |
| Round-robin assignment | Not available | Replaced by Flow-based push routing |
| Approval workflows | No native approval workflow | Custom implementation via Flows or external tools |
Dixa API Constraints
- Import channels limited to
emailandgenericapimessaging. No way to import historical phone calls or live chats with their native channel metadata. - 10 requests/second per token. For large migrations, use multiple tokens to increase throughput.
- No bulk import endpoint. Each conversation requires its own API call.
- No idempotency. Re-importing the same record creates duplicates. Your pipeline must handle deduplication.
- Dixa does not publish a single fixed payload size limit across the API; individual endpoints set their own batch ceilings (per Dixa API documentation).
- Sandbox access starts at Ultimate tier. Test migrations require the right Dixa plan, or coordinate with Dixa support for temporary sandbox access.
Desk365 API Constraints
- The 10,000 tickets/hour extraction cap applies per session. For large datasets, batch extractions over multiple sessions or time windows.
- Desk365's Standard plan may have lower API limits — verify your plan's specific caps before building extraction scripts.
- The API key is per-account, not per-user. If the API key is regenerated during migration, all in-progress extraction scripts will fail with 401.
Validation and Testing
Data validation prevents post-migration disasters. Do not rely solely on a "200 OK" API response — Dixa may accept a payload but produce a conversation with missing messages or attributes.
Record Count Comparison
| Metric | Expected Match |
|---|---|
| Total conversations | Desk365 ticket count (minus excluded) |
| Total end users | Deduplicated Desk365 contact count |
| Messages per conversation | Desk365 replies + 1 (original description), minus private notes |
| Tags | All Desk365 categories/types mapped to tags |
| Custom attributes | All mapped fields populated |
| Internal notes | Count of Desk365 private notes |
Field-Level Validation
- Pull a random sample of 50–100 conversations from Dixa via
GET /v1/conversations/{id} - Compare subject, requester email, created timestamp, and message count against the Desk365 source
- Verify custom attribute values match the original custom field values
- Confirm attachments are accessible in Dixa (download each one)
- Check that original creation dates, assignees, and requester info match
- Verify internal notes appear only as internal notes — not as customer-visible messages
- Confirm that inline images render correctly
UAT Process
- Migrate a test batch of 500–1,000 tickets first into a Dixa sandbox (requires Ultimate tier or Dixa support coordination)
- Have 2–3 senior support agents review their own historical tickets in Dixa
- Verify search works for migrated conversations (search by subject, customer email, ticket number custom attribute)
- Confirm that imported conversations do not trigger Dixa Flows (imported conversations bypass routing — verify this)
- If agents cannot find historical context for a customer, the migration mapping needs adjustment
Rollback Planning
Dixa does not offer a bulk delete API for imported conversations. Your rollback plan is:
- Keep Desk365 active and accessible throughout the migration
- If migration fails, continue operating in Desk365
- Delete Dixa test data via support request to Dixa before attempting a clean re-import
- Your staging database serves as the source of truth for what was imported and what needs to be cleaned up
For a complete testing framework, refer to our Help Desk Data Migration QA Checklist.
Knowledge Base Migration
Knowledge base migration is a separate workstream from ticket migration. Desk365's built-in KB stores articles with categories, rich text content, and embedded media.
Approach:
- Export KB articles from Desk365 (manual export or API if available)
- Transform article HTML to match Dixa's KB content format
- Re-upload embedded images to Dixa-compatible hosting
- Create category structure in Dixa KB
- Import articles via Dixa's KB API or manual creation
Key considerations:
- KB article URLs will change — update any links in canned responses, macros, or external documentation
- SEO redirects from old Desk365 KB URLs to new Dixa KB URLs
- Internal links between KB articles must be updated to new URLs
- Embedded media must be re-hosted
For large KB libraries (500+ articles), script the migration. For smaller libraries, manual recreation may be faster than building extraction tooling.
Post-Migration Tasks
Once the data is in Dixa, the operational transition begins.
Rebuild Automations in Dixa
Desk365 automation rules, SLA policies, and canned responses cannot be migrated programmatically. Rebuild them:
- Automation rules → Dixa Flows. Dixa's visual Flow Builder replaces rule-based automations. Map each Desk365 rule to a Flow path. Document each Desk365 automation rule before migration and create the corresponding Flow in Dixa.
- SLA policies → Dixa SLAs. Reconfigure response and resolution targets in Dixa's SLA settings. Match the exact thresholds from Desk365.
- Canned responses → Quick Replies. Export canned responses from Desk365 and recreate them in Dixa's agent workspace. This is manual — budget 1–2 hours per 50 canned responses.
Update Integrations
Reconnect your CRM (e.g., Salesforce, HubSpot) to Dixa. If pipeline or company data lives in a CRM, surface it through Dixa's custom cards or integration layer — do not try to recreate CRM objects inside Dixa.
Agent Training
Dixa's push-based routing is a fundamental workflow change from Desk365's pull-based views. Agents need training on:
- How conversations are offered vs. manually selected
- The Flow Builder and queue structure
- Where migrated history appears in the customer timeline
- How to use tags and custom attributes for filtering
- How to search for migrated tickets using the
desk365_ticket_idcustom attribute
Expect a behavioral shift from "cherry-picking" tickets to accepting push-routed conversations. Plan 2–4 hours of hands-on training per agent.
Monitor for Data Inconsistencies
Run daily spot checks for the first two weeks post-migration. Watch for:
- Missing conversations (compare counts daily against your migration log)
- Conversations assigned to wrong agents
- Custom attributes with null values
- Attachments returning 404
- Internal notes appearing as customer-visible messages
- Duplicate conversations (evidence of deduplication failure)
Decommission Desk365
Keep Desk365 in read-only mode for 30 days post-migration to ensure no edge-case data was missed. After confirmation:
- Export a final complete backup of all Desk365 data
- Terminate the license
- Archive the backup for compliance/audit requirements
Best Practices
- Back up everything before migration. Desk365 offers several export options including standard ticket view, current ticket view, custom view, search tickets, and time entries (per Desk365 export documentation). Export all tickets and contacts to CSV as a safety net.
- Never migrate directly to production first. Always run a test migration of at least 1,000 records into a Dixa sandbox environment.
- Create agents and teams in Dixa first. Agent IDs must exist before you can assign outbound messages to them.
- Freeze schema changes. Implement a change freeze on Desk365 custom fields two weeks before the migration.
- Preserve original Desk365 ticket numbers. Store the original ticket ID as a custom attribute on the Dixa conversation for cross-reference and searchability.
- Implement deduplication in your staging layer. Dixa's import endpoint is not idempotent. Track every successfully imported record by source ID.
- Use multiple API tokens for load. If Dixa allows multiple tokens on your plan, parallelize the import to increase throughput.
- Log everything. Every API call, response code, and error should be logged with timestamps. You will need this for validation and debugging.
- Plan the cutover window. Even with a delta sync, plan for a 2-hour window where inbound emails are queued at the DNS level while final routing rules are activated in Dixa.
- Validate incrementally. Check counts and samples after each phase, not only at the end.
- Handle inline images proactively. Parse HTML bodies for base64 images and rehost them before import.
Sample Data Mapping Table
| Desk365 Field | Desk365 API Key | Dixa Field | Dixa API Key | Transformation |
|---|---|---|---|---|
| Ticket Number | ticket_number |
Custom Attribute | Custom attribute UUID | Store as desk365_ticket_id for audit and traceability |
| Subject | subject |
Subject | subject |
Direct map |
| Description | description |
First Message | messages [0].content.value |
HTML to Dixa content; rehost inline images |
| Status | status |
State | state |
Map to open/closed; preserve original as tag |
| Priority | priority |
Tag | tags |
Create priority tags (e.g., priority:high) |
| Type | type |
Tag | tags |
Map types to tags (e.g., desk365_type:Incident) |
| Contact Email | contact_email |
Requester | requesterId |
Resolve via End User API first |
| Contact Name | contact_name |
End User Display Name | displayName |
Direct map |
| Company | company_name |
Custom Attribute | custom_attributes |
Create company attribute on end user |
| Group | group |
Queue | Queue assignment | Map operational ownership to queue IDs |
| Assigned Agent | assigned_to |
Agent | agentId |
Map by email to Dixa agent UUID |
| Created Date | created_time |
Created At | createdAt |
ISO 8601 UTC format |
| Custom Field | cf_{field_name} |
Custom Attribute | Attribute UUID | Type conversion required; flatten multi-level |
| Tags | tags |
Tags | Tag IDs | Create tags in Dixa first, then apply via API |
| Private Notes | via conversations API | Internal Notes | POST /v1/conversations/{id}/notes |
Import separately after conversation |
| Attachments | attachment URLs | Attachment URLs | attachments array |
Host on S3 if Desk365 URLs expire |
| Channel | source |
Tag | Custom attribute | Store as desk365_channel tag for provenance |
Making the Decision
A Desk365-to-Dixa migration is a moderate-complexity project on the data side and a high-complexity project on the workflow side. The data transfers well — tickets become conversations, contacts become end users, and custom fields map to custom attributes. The hard part is accepting the structural compromises (no company object, binary status model, no multi-level fields, no import idempotency) and investing the time to rebuild Desk365's automations as Dixa Flows.
For teams with fewer than 10,000 tickets and simple configurations, a scripted DIY migration is viable at 60–100 engineer-hours. For anything more complex — custom fields, multi-level dependencies, large datasets, or tight cutover windows — bring in a team that has done this before. Start with a mapping spec, run a pilot of 500–1,000 tickets, and cut over in batches.
For related migrations, see Freshdesk to Dixa Migration and Zendesk to Dixa Migration.
Frequently Asked Questions
- Can I migrate Desk365 tickets to Dixa using CSV?
- Only partially. Desk365 exports tickets as CSV, but Dixa has no native CSV import for conversations — you still need the API for loading. CSV exports also exclude full reply threads and attachments, so they are only useful for small contact/company seeds or audit extracts.
- What Dixa API channels are supported for importing historical conversations?
- Dixa's POST /v1/conversations/import endpoint only supports 'email' and 'genericapimessaging' channel types. Phone calls, live chats, Teams-originated tickets, and social messages from Desk365 must be normalized into one of these two types during migration.
- How long does a Desk365 to Dixa migration take?
- A typical migration for 50,000–150,000 tickets takes 2–4 weeks including data mapping, test migrations, and cutover. Dixa's API rate limit (10 requests/second per token, 864,000 daily ceiling) is the primary bottleneck for the data load phase.
- Does Dixa support custom fields from Desk365?
- Yes, but with structural differences. Desk365's custom ticket fields (including multi-level dropdowns) must be mapped to Dixa's flat custom attributes. Multi-level fields need to be flattened — either concatenated into a single text value or split into separate attributes.
- Can Desk365 automations and SLAs be migrated to Dixa?
- No. Desk365 automation rules, SLA policies, and canned responses cannot be programmatically migrated. They must be manually rebuilt in Dixa using the visual Flow Builder, SLA configuration, and Quick Replies.

