---
title: "Desk365 to Dixa Migration: A Technical Guide"
slug: desk365-to-dixa-migration-a-technical-guide
date: 2026-07-31
author: Raaj
categories: [Migration Guide, Dixa]
excerpt: "Technical guide for migrating from Desk365 to Dixa — data model mapping, API limits, transformation logic, and step-by-step process for zero-downtime cutover."
tldr: "Desk365 tickets map to Dixa conversations via API, but Dixa import only supports email and genericapimessaging channels. Plan 2–4 weeks for 50K+ tickets."
canonical: https://clonepartner.com/blog/desk365-to-dixa-migration-a-technical-guide/
---

# Desk365 to Dixa Migration: A Technical Guide


# Desk365 to Dixa Migration: A Technical Guide

> [!NOTE]
> **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](https://clonepartner.com/blog/blog/help-desk-data-migration-qa-checklist/). For Dixa-side extraction details, see [How to Export Data from Dixa: Methods, API Limits & Data Mapping](https://clonepartner.com/blog/blog/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](https://clonepartner.com/blog/blog/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:

1. **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.
2. **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.
3. **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 `email` and `genericapimessaging` only (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/import` endpoint 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](https://clonepartner.com/blog/blog/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](#knowledge-base-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:

1. **Custom attribute on end users.** Create a `company_name` custom attribute on Dixa end users and populate it during contact import.
2. **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 |

> [!WARNING]
> **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_count` parameter (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:** `email` and `genericapimessaging` only
- **Error handling:** Exceeding the rate limit returns HTTP 429 Too Many Requests. Dixa does not send a `Retry-After` header — 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:

1. Navigate to **Settings > Integrations > Email** in the Dixa admin panel
2. Select your email integration (e.g., `support@yourdomain.com`)
3. The integration ID appears in the URL: `https://app.dixa.io/settings/integrations/email/{integrationId}`
4. Alternatively, call `GET /v1/integrations/email` via 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.

```python
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.

```python
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.

```python
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.

```python
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 result
```

### Step 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.

```python
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](#validation-and-testing) section below.

> [!TIP]
> **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:
1. Parse the HTML body for `<img>` tags with `src="data:image/..."` attributes
2. Extract the base64 string
3. Decode and upload to hosted storage (e.g., S3, Azure Blob Storage)
4. Rewrite the `<img>` tag `src` attribute 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 | `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 `email` and `genericapimessaging`.** 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

1. Pull a random sample of 50–100 conversations from Dixa via `GET /v1/conversations/{id}`
2. Compare subject, requester email, created timestamp, and message count against the Desk365 source
3. Verify custom attribute values match the original custom field values
4. Confirm attachments are accessible in Dixa (download each one)
5. Check that original creation dates, assignees, and requester info match
6. Verify internal notes appear only as internal notes — not as customer-visible messages
7. Confirm that inline images render correctly

### UAT Process

1. Migrate a test batch of 500–1,000 tickets first into a Dixa sandbox (requires Ultimate tier or Dixa support coordination)
2. Have 2–3 senior support agents review their own historical tickets in Dixa
3. Verify search works for migrated conversations (search by subject, customer email, ticket number custom attribute)
4. Confirm that imported conversations do not trigger Dixa Flows (imported conversations bypass routing — verify this)
5. 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:

1. Keep Desk365 active and accessible throughout the migration
2. If migration fails, continue operating in Desk365
3. Delete Dixa test data via support request to Dixa before attempting a clean re-import
4. 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](https://clonepartner.com/blog/blog/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:**
1. Export KB articles from Desk365 (manual export or API if available)
2. Transform article HTML to match Dixa's KB content format
3. Re-upload embedded images to Dixa-compatible hosting
4. Create category structure in Dixa KB
5. 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_id` custom 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:

1. Export a final complete backup of all Desk365 data
2. Terminate the license
3. Archive the backup for compliance/audit requirements

## Best Practices

1. **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.
2. **Never migrate directly to production first.** Always run a test migration of at least 1,000 records into a Dixa sandbox environment.
3. **Create agents and teams in Dixa first.** Agent IDs must exist before you can assign outbound messages to them.
4. **Freeze schema changes.** Implement a change freeze on Desk365 custom fields two weeks before the migration.
5. **Preserve original Desk365 ticket numbers.** Store the original ticket ID as a custom attribute on the Dixa conversation for cross-reference and searchability.
6. **Implement deduplication in your staging layer.** Dixa's import endpoint is not idempotent. Track every successfully imported record by source ID.
7. **Use multiple API tokens for load.** If Dixa allows multiple tokens on your plan, parallelize the import to increase throughput.
8. **Log everything.** Every API call, response code, and error should be logged with timestamps. You will need this for validation and debugging.
9. **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.
10. **Validate incrementally.** Check counts and samples after each phase, not only at the end.
11. **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](https://clonepartner.com/blog/blog/how-to-migrate-from-freshdesk-to-dixa-the-complete-technical-guide/) and [Zendesk to Dixa Migration](https://clonepartner.com/blog/blog/zendesk-to-dixa-migration-the-complete-technical-guide/).

> Planning a Desk365 to Dixa migration? We'll review your Desk365 data model, map the Dixa structure, and give you an honest assessment of scope — in a free 30-minute call.
>
> [Talk to us](https://cal.com/clonepartner/meet?duration=30)

## 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.
